Repository: conan-io/conan
Pull request: Add MakeDeps generator
Changelog: Feature: Add support to Makefile by the new MakeDeps generator
Docs: https://github.com/conan-io/docs/pull/3348
closes #13394

This PR adds a new feature, the `MakeDeps` generator, that can be used with Conan install command: `conan install --requires=foobar/0.1.0 -g MakeDeps`. The idea is bringing back the `make` generator from Conan 1.x but with steroids, including support for components.
As real example, using Poco package, we will have:

```
% conan install --requires=poco/1.12.1 -g MakeDeps 
% ls *.mk
conandeps.mk
``` 

The are 3 levels of dependencies here:

* Each Poco component and its dependencies will generate separated variables, that contains values related to that component, plus, dependencies;
* Then, each package, include Poco, will have variable with all components listed, and all variables appending components;
* As top level in conandeps.mk, all global variables like CONAN_LIB_DIRS, will list all dependencies.


- [x] Refer to the issue that supports this Pull Request.
- [x] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [x] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've followed the PEP8 style guides for Python code.
- [x] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one.


Problem statement:
[question] What's the intended way to work with makefiles without autotools in 2.0
### What is your question?

I'm new to Conan with 2.0 and my team is currently using custom makefiles without autotools to build our projects. Googling around I end up on 1.x docs of Conan where there does seem to be built in generators for makefiles. So how do I work with makefiles in 2.0?

Packaging at least seems easy. I can just not have a `build()` function and use a wrapper script to build with make first and then call `conan create`. Maybe I could have the `build()` function call the python `subprocess` library to handle the build itself though?

However consuming conan packages is not as straight forward. Should I just be using the `VirtualBuildEnv` and relying on env vars in my makefiles?

Are there any good example projects that build with makefiles and conan 2.0?

### Have you read the CONTRIBUTING guide?

- [X] I've read the CONTRIBUTING guide

Hints from issue discussion:
Hi @daltonv 

Not sure if you are aware of the ``conan new autotools_lib -d name=pkg -d version=0.1``.
It is true that it is an autotools project, not a pure Makefile one, but I think stripping out the autotools part should be relatively easy and the overall approach should be a good start.

> I can just not have a build() function and use a wrapper script to build with make first and then call conan create. 

Sorry, I don't know what you mean. Typically running ``self.run("make ...")`` should be the way to go. In general running ``subprocess`` yourself is not recommended, but using the ``self.run()``

Yes, the ``test_package/conanfile.py`` inside the template would be almost a typical consumer project. Not ``VirtualBuildEnv``, but depending on what you are doing ``AutotoolsDeps`` or ``PkgConfigDeps`` should probably be the way to go. Even if it is called ``AutotoolsDeps`` it is mostly generating environment scripts with the environment variables necessary to locate the dependencies, nothing really strictly of "autotools" only. Probably same for ``AutotoolsToolchain``.


I'd recommend giving a try to the template, removing the autotools bits, and if you have any issues, let us know and we could try to put together an example. Thanks!

Actually, this might be useful for some other users, maybe we want to make a ``conan new makefile_lib`` (and _exe) templates too.
Ah I didn't see the `ConanFile` class has a `self.run()` method. That makes it easy.

I'll try out the autotools project and report back.

I definitely think that a makefile example would be helpful. I'm working on embedded projects, and while I can control my build system, many embedded sdks and libraries still use plain makefiles to build. 
> Ah I didn't see the ConanFile class has a self.run() method. That makes it easy.

Yes, and beyond easier, the ``self.run()`` might do other stuff, like automatically inject environment variables scripts.

> I'll try out the autotools project and report back.

I definitely think that a makefile example would be helpful. I'm working on embedded projects, and while I can control my build system, many embedded sdks and libraries still use plain makefiles to build.

Perfect, looking forward your feedback, please share you findings and we will propose a new pure Makefiles template. Thanks!

Okay it was actually pretty easy to get the makefiles running now that I know where to start! I used the `AutotoolsDeps` generator as suggested.

A couple points of feedback:

## Docs

Initially I thought I just missed something in the docs about the `self.run()` method, and I did, but I feel like it could have been more obvious. It's indirectly talked about in the [build() tutorial](https://docs.conan.io/2/tutorial/creating_packages/build_packages.html#changes-introduced-in-the-recipe) but I missed that on my first glance. The only other obvious place I saw it was in reference material for [build()](https://docs.conan.io/2/reference/conanfile/methods/build.html). I think the docs should at least have a reference section about the `run()` command as currently you have to look at the source code to understand the args. Additionally a makfile example would probably make it more obvious that the `run()` command exists.

## [tool_requires] flags

Currently the `AutotoolsDeps` generator does not include dependencies from [tool_requires] settings. The [docs](https://docs.conan.io/2/reference/tools/gnu/autotoolsdeps.html#generated-files) seem to imply that this will be included in the future. I really hope so, because I want to package the arm gcc compiler to be used in all my projects, and there are some arm specific cflags I want all projects to use. So it would be nice if the compiler package provides those to downstream projects.

I can work around this by just adding those settings directly in the makefile, but it's much cleaner to cross compiler for x86 and various arm archs in `CMake` land as the [tool_requires] `cpp_info` settings are carried over. 

Even if I put the cflags in my profile file `AutotoolsDeps` doesn't pick them up. Example:
```ini
[conf]
tools.build:cflags+=["-gstrict-dwarf", "-march=armv6-m"]
```

Overall though it's pretty easy to setup makefile projects with `AutotoolsDeps` + `self.run(make)`. I think an example that shows this would be very nice. 

Just need to generate with `AutotoolsDeps` and call `self.run(make)` the `package()` function will likely be custom for each makefile project. 

Within the files created by `conan new autotools_lib` there is a folder called `test_package` which I believe is an example of a project that "consumes" the library built. However, it is still autotools... I mean `autotools` is required if I want the consumer test_package to build without running `conan build ...` right?

The same is true with other generators (e.g., cmake), `conan install .` on the consumer project will create the files that then my consumer's `CMakeLists.txt` can include them in order to pull in the library helper vars needed to provide the CPPFLAGS/LDFLAGS/etc for building. With those generators, I can then run `cmake` followed by  `make` directly to build my consumer project (i.e., I don't have to run `conan build`).

I don't see how the same thing can be done with the `autotools_lib` template and a standard Makefile project. I don't see any files that are generated by `Autotoolsdeps` generator that a `Makefile` could include to build the consumer project where I simply run "make" and the consumer project builds.  Am I missing something? Or maybe I'm misunderstanding the workflow?
Hey @gabeblack. The generated `conanbuild.sh` script and friends from `AutotoolsDeps` can inject info into the makefile. This is because makefiles will consume environment variables.

So the correct flow will be:
1. Run `conan install .`
2. Run `source conanbuild.sh` - This loads the shell scripts that set env vars for stuff like `CC`, `CFLAGS`, etc.
3. Run `make` - Your makefile should be using the special env vars. Like `$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@`.

If you add `self. run(make)` to your `conanfile.py` (and correctly generated the `AutotoolsDeps`) the `run()` method automagically loads the env vars for you, so you skip step 2, and `conan build .` handles it for you. 
I've had a very similar experience to the OP and landing on this issue was the help I needed since I was getting the impression that since it was in the 1.x docs but not the 2.x that this kind of support had been removed.

I started from the Autotools template and removed things from there, although I still use `Autotools.make()` and `Autotools.install()` instead of `self.run()` as all they do is call the correct things anyway. Having mention of Autotools at all does make the contents of my `conanfile.py` a bit misleading, but I am also using `generators = "AutotoolsToolchain", "AutotoolsDeps"` to get the environment set up. Maybe some sort of alias or explicit support would be good.

Anyway, the upshot is that I can retro-fit old `make` based projects to use Conan 2.0 successfully with this approach. Just posting this to echo the OP's thoughts, emphasis the use-case, and reassure anyone else coming across this issue.
Hello folks,

I am currently looking into this issue myself. One problem with the `AutotoolsDeps` generator (and many others) is that it only aggregates the variables of multiple packages. For example, in the generator output, `CPPFLAGS` contains all the `-Idirectory` flags for the various packages, concatenated together. Which, to be sure, is fine for many use cases. But if you need the `CPPFLAGS` variable specifically for the `foobaz` package, then you're out of luck---there is no `CONAN_INCLUDE_DIRS_FOOBAZ` variable that can be referenced. You'll have to do a `conan install` for that package alone.

Also, it's not clear to me if/how the `AutotoolsDeps` generator addresses some of the other variables provided by the `make` one, like `CONAN_RES_DIRS_FOOBAZ`. As far as I can tell, it only deals with the official GNU-recognized environment variables.

Is there any work in progress on a Make-compatible generator that will provide per-package variables? Would a PR to port the old `make` generator to 2.0 (or perhaps develop a new one) be OK in principle?
@iskunk -- I actually ran in to the same thing. So yes, you can make the `Autotools`/`AutotoolsDeps` work for many makefile packages, but it is incomplete for the reason you gave. 

I ended up having to sort of write my own generator to do what you and I were needing. The following will create a `CONAN_<pkg_name>_PKG_FOLDER` which from that you can tag on "include", "libs", etc to get the include, libs, etc folders. Or you can extend this implementation to make the package-specific variables. I also had it rename the global CPPFLAGS to be CONAN_CPPFLAGS, LDFLAGS to CONAN_LDFLAGS, etc, etc. In my use-case, I didn't want the conan CPPFLAGS to be used for my entire project. Just parts. So I renamed the global variables conan creates and prefixed them with 'CONAN_'. Finally, this generator creates a file that can be included with make. So 'include conanbuild.make' will work and you don't need to source environment prior to running the build.

TBH, it would be nice if they just brought back the makefile generator support. If people aren't against it, maybe a variation of what I have done here could be added to conan. 

```
import re
from conan import ConanFile
from conan.tools.files import copy, save
from conan.tools.gnu import AutotoolsToolchain, AutotoolsDeps


class MakefileDeps:
    # conan 2.0 deprecated the makefile generator... so implementing a simple makefile generator
    def __init__(self, conanfile):
        self._conanfile = conanfile

    def generate(self):
        autotools = AutotoolsDeps(self._conanfile)
        env_vars = autotools.environment.vars(self._conanfile)
        autotools.generate()

        content = []

        # Rename the global CPPFLAGS, LDFLAGS, etc., with a "CONAN_" prefix --  CONAN_CPPFLAGS, CONAN_LDFLAGS, ...
        for key, value in env_vars.items("${name}"):
            value = re.sub(r"\$(\w+)", r"$(\1)", value)
            value = value.replace('"', '\\"')
            content.append(re.sub(f"{key}", f"CONAN_{key}", f"{key} := {value}"))

        # add CONAN_<PKG_NAME>_PKG_FOLDER := <path/to/package/folder> variables
        for require, dep in self._conanfile.dependencies.items():
            package_name = re.sub("-", "_", dep.ref.name).upper()
            if dep.package_folder:
                content.append(f"CONAN_{package_name}_PKG_FOLDER := {dep.package_folder}")
        save(self._conanfile, f"{self._conanfile.generators_folder}/conanbuild.make", "\n".join(content))

```

Commit message: Add Make (and shell) generator (#13394)

Patch to review:
diff --git a/conan/tools/misc/__init__.py b/conan/tools/misc/__init__.py
new file mode 100644
index 00000000000..0351dc5e13b
--- /dev/null
+++ b/conan/tools/misc/__init__.py
@@ -0,0 +1,2 @@
+from conan.tools.misc.makedeps import MakeDeps
+from conan.tools.misc.shdeps import ShDeps
diff --git a/conan/tools/misc/makedeps.py b/conan/tools/misc/makedeps.py
new file mode 100644
index 00000000000..e6d6bec1fa1
--- /dev/null
+++ b/conan/tools/misc/makedeps.py
@@ -0,0 +1,175 @@
+# -*- coding: utf-8 -*-
+
+import os
+import re
+
+from conan.internal import check_duplicated_generator
+from conan.tools.files import save
+from conans.model.build_info import CppInfo
+
+class MakeDeps(object):
+
+    _output_filename = "conandeps.mk"
+    _title = "Makefile variables from Conan dependencies"
+
+    def __init__(self, conanfile):
+        """
+        :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``.
+        """
+        self._conanfile = conanfile
+
+    def _var_ref(self, var):
+        return f"$({var})"
+
+    def _var_assign_single(self, var, value):
+        return f"{var} = {value}"
+
+    def _var_assign_multi(self, var, value):
+        return " \\\n\t".join([f"{var} ="] + value)
+
+    def generate(self):
+        check_duplicated_generator(self, self._conanfile)
+
+        line_buffer = []
+        var_aggregates = {}
+
+        def _divert():
+            buf = list(line_buffer)
+            line_buffer.clear()
+            return buf
+
+        # Convert a name to Make-variable-friendly syntax
+        def _makeify(name):
+            return re.sub(r'[^0-9A-Z_]', '_', name.upper())
+
+        # Replace common prefixes in a path list with a variable reference
+        def _rootify(root, root_id, path_list):
+            root_len = len(root)
+            root_with_sep = root + os.path.sep
+            root_var_ref = self._var_ref(f"CONAN_ROOT_{root_id}")
+            return [root_var_ref + path[root_len:] \
+                    if path.startswith(root_with_sep) else path for path in path_list]
+
+        # Output a variable assignment, appropriately formatted
+        def _output_var(name, item, root=None, root_id=None, prefix_var=None, blank_after=True):
+            if item is None:
+                return False
+            if type(item).__name__ in ('Component', 'SettingsItem', 'Version', 'str'):
+                value = str(item)
+                if not value:
+                    return False
+                line_buffer.append(self._var_assign_single(name, value))
+            else:
+                assert type(item) is list
+                ritem = _rootify(root, root_id, item) if root else item
+                if prefix_var:
+                    ritem = [self._var_ref(prefix_var) + i for i in ritem]
+                if len(ritem) == 0:
+                    return False
+                elif len(ritem) == 1:
+                    line_buffer.append(self._var_assign_single(name, ritem[0]))
+                else:
+                    line_buffer.append(self._var_assign_multi(name, ritem))
+            if blank_after:
+                line_buffer.append("")
+            return True
+
+        def _output_cpp_info_vars(var_id, cpp_info, root=None, root_id=None, add_to_aggregates=True):
+            def _var(name, item, prefix_var=None):
+                v = f"CONAN_{name}_{var_id}"
+                if _output_var(v, item, root, root_id, prefix_var) and add_to_aggregates:
+                    var_aggregates.setdefault(name, []).append(self._var_ref(v))
+            _var("SYSROOT",         cpp_info.sysroot)
+            _var("INCLUDE_DIRS",    cpp_info.includedirs, "CONAN_INCLUDE_DIR_FLAG")
+            _var("LIB_DIRS",        cpp_info.libdirs,     "CONAN_LIB_DIR_FLAG")
+            _var("BIN_DIRS",        cpp_info.bindirs)
+            _var("SRC_DIRS",        cpp_info.srcdirs)
+            _var("BUILD_DIRS",      cpp_info.builddirs)
+            _var("OBJECTS",         cpp_info.objects)
+            _var("RES_DIRS",        cpp_info.resdirs)
+            _var("LIBS",            cpp_info.libs,        "CONAN_LIB_FLAG")
+            _var("SYSTEM_LIBS",     cpp_info.system_libs, "CONAN_SYSTEM_LIB_FLAG")
+            _var("DEFINES",         cpp_info.defines,     "CONAN_DEFINE_FLAG")
+            _var("CFLAGS",          cpp_info.cflags)
+            _var("CXXFLAGS",        cpp_info.cxxflags)
+            _var("SHAREDLINKFLAGS", cpp_info.sharedlinkflags)
+            _var("EXELINKFLAGS",    cpp_info.exelinkflags)
+            _var("FRAMEWORKS",      cpp_info.frameworks)
+            _var("FRAMEWORK_DIRS",  cpp_info.frameworkdirs)
+            _var("REQUIRES",        cpp_info.requires)
+
+        # Top-of-file banner
+        line_buffer.extend([
+            "#" * 72,
+            "##" + " " * 68 + "##",
+            "##" + self._title.center(68) + "##",
+            "##" + " " * 68 + "##",
+            "#" * 72,
+            ""
+        ])
+
+        # Output settings variables
+        for key, value in self._conanfile.settings.items():
+            var_name = "CONAN_" + _makeify(key)
+            _output_var(var_name, value, blank_after=False)
+        line_buffer.append("")
+
+        preamble = _divert()
+
+        body = []
+        dep_name_list = []
+
+        for require, dep in self._conanfile.dependencies.items():
+            dep_id = _makeify(require.ref.name)
+            dep_name_list.append(require.ref.name)
+
+            root = dep.package_folder
+
+            line_buffer.extend([
+                "",
+                f"# {require.ref.name}/{require.ref.version}",
+                f"# (" + ("direct" if require.direct else "indirect") + " dependency)",
+                ""
+            ])
+
+            _output_var(f"CONAN_ROOT_{dep_id}",    root)
+            _output_var(f"CONAN_VERSION_{dep_id}", require.ref.version)
+
+            _output_cpp_info_vars(dep_id, dep.cpp_info, root, dep_id)
+
+            dep_section = _divert()
+            comp_name_list = []
+
+            for comp_name, component in dep.cpp_info.components.items():
+                line_buffer.append(f"# {require.ref.name}::{comp_name}")
+                line_buffer.append("")
+                comp_id = _makeify(comp_name)
+                comp_name_list.append(comp_name)
+                _output_cpp_info_vars(f"{dep_id}_{comp_id}", component, root, dep_id, False)
+
+            comp_section = _divert()
+
+            line_buffer.extend(dep_section)
+            _output_var(f"CONAN_COMPONENTS_{dep_id}", comp_name_list)
+            line_buffer.extend(comp_section)
+
+            body.extend(_divert())
+
+        assert not line_buffer
+        line_buffer.extend([
+            "",
+            "# Aggregated values",
+            "# (note: these do not include components)",
+            ""
+        ])
+        for var_name, var_list in var_aggregates.items():
+            _output_var(f"CONAN_{var_name}", var_list)
+        body.extend(_divert())
+
+        line_buffer.extend(preamble)
+        _output_var("CONAN_DEPS", dep_name_list)
+        line_buffer.extend(body)
+        line_buffer.extend(["", "#" * 72])
+
+        save(self._conanfile, self._output_filename, "\n".join(line_buffer))
+        self._conanfile.output.info(f"Generated {self._output_filename}")
diff --git a/conan/tools/misc/shdeps.py b/conan/tools/misc/shdeps.py
new file mode 100644
index 00000000000..166b252724f
--- /dev/null
+++ b/conan/tools/misc/shdeps.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+from conan.tools.misc.makedeps import MakeDeps
+
+class ShDeps(MakeDeps):
+
+    _output_filename = "conandeps.sh"
+    _title = "Shell variables from Conan dependencies"
+
+    def __init__(self, conanfile):
+        """
+        :param conanfile: ``< ConanFile object >`` The current recipe object. Always use ``self``.
+        """
+        super().__init__(conanfile)
+
+    def _var_ref(self, var):
+        return f"${{{var}}}"
+
+    def _var_assign_single(self, var, value):
+        return f"{var}=\"{value}\""
+
+    def _var_assign_multi(self, var, value):
+        return f"{var}=\"" + " ".join(value) + "\""
diff --git a/conans/client/generators/__init__.py b/conans/client/generators/__init__.py
index ad9f8b88771..26b1996d31b 100644
--- a/conans/client/generators/__init__.py
+++ b/conans/client/generators/__init__.py
@@ -21,6 +21,8 @@
                "IntelCC": "conan.tools.intel",
                "XcodeDeps": "conan.tools.apple", "XcodeToolchain": "conan.tools.apple",
                "PremakeDeps": "conan.tools.premake",
+               "MakeDeps": "conan.tools.misc",
+               "ShDeps": "conan.tools.misc",
                }
 
 


Code review comments:
diff_hunk: @@ -0,0 +1,213 @@
+import os.path
+import platform
+import re
+import textwrap
+
+import pytest
+
+from conans.test.assets.genconanfile import GenConanfile
+from conans.test.utils.tools import TestClient
+
+
+@pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Make")
path: conans/test/integration/toolchains/gnu/test_makedeps.py
text: @user1:
The test doesn't really execute Make, so it should run in Windows too?

@author:
sorry, those tests are not ready, I need to change many things here.

diff_hunk: @@ -0,0 +1,602 @@
+import os
+import re
+import textwrap
+
+from collections import namedtuple
+from jinja2 import Template, StrictUndefined
+
+
+from conan.internal import check_duplicated_generator
+from conan.tools.files import save
+from conans.errors import ConanException
+from conans.model.dependencies import get_transitive_requires
+
+
+def _get_name_with_namespace(namespace, name):
+    """ Build a name with a namespace, e.g., openssl-crypto
+    :param namespace: package name
+    :param name: component name
+    :return: name with namespace
+    """
+    if namespace == name or name.startswith(namespace):
+        return name
+    return f"{namespace}-{name}"
+
+
+def _get_package_reference_name(dep):
+    """ Get the reference name for the given package
+    :param dep: package reference instance
+    :return: reference name
+    """
+    return dep.ref.name
+
+
+def _get_package_aliases(dep):
+    pkg_aliases = dep.cpp_info.get_property("make_aliases")
+    return pkg_aliases or []
+
+
+def _get_component_aliases(dep, comp_name):
+    if comp_name not in dep.cpp_info.components:
+        # foo::foo might be referencing the root cppinfo
+        if _get_package_reference_name(dep) == comp_name:
+            return _get_package_aliases(dep)
+        raise ConanException("Component '{name}::{cname}' not found in '{name}' "
+                             "package requirement".format(name=_get_package_reference_name(dep),
+                                                          cname=comp_name))
+    comp_aliases = dep.cpp_info.components[comp_name].get_property("make_aliases")
+    return comp_aliases or []
+
+
+def _get_package_name(dep, build_context_suffix=None):
+    pkg_name = dep.cpp_info.get_property("make_name") or _get_package_reference_name(dep)
path: conan/tools/gnu/makedeps.py
text: @user1:
Why custom make names are necessary? As there is no standard in this regard, and Conan is inventing this integration, I think we can mandate the makefile names to always match package names and components, no need for properties.

@author:
Indeed not needed, I'll remove it, one less feature to be maintained/tested.

diff_hunk: @@ -0,0 +1,602 @@
+import os
+import re
+import textwrap
+
+from collections import namedtuple
+from jinja2 import Template, StrictUndefined
+
+
+from conan.internal import check_duplicated_generator
+from conan.tools.files import save
+from conans.errors import ConanException
+from conans.model.dependencies import get_transitive_requires
+
+
+def _get_name_with_namespace(namespace, name):
+    """ Build a name with a namespace, e.g., openssl-crypto
+    :param namespace: package name
+    :param name: component name
+    :return: name with namespace
+    """
+    if namespace == name or name.startswith(namespace):
+        return name
+    return f"{namespace}-{name}"
+
+
+def _get_package_reference_name(dep):
+    """ Get the reference name for the given package
+    :param dep: package reference instance
+    :return: reference name
+    """
+    return dep.ref.name
+
+
+def _get_package_aliases(dep):
+    pkg_aliases = dep.cpp_info.get_property("make_aliases")
+    return pkg_aliases or []
+
+
+def _get_component_aliases(dep, comp_name):
+    if comp_name not in dep.cpp_info.components:
+        # foo::foo might be referencing the root cppinfo
+        if _get_package_reference_name(dep) == comp_name:
+            return _get_package_aliases(dep)
+        raise ConanException("Component '{name}::{cname}' not found in '{name}' "
+                             "package requirement".format(name=_get_package_reference_name(dep),
+                                                          cname=comp_name))
+    comp_aliases = dep.cpp_info.components[comp_name].get_property("make_aliases")
+    return comp_aliases or []
+
+
+def _get_package_name(dep, build_context_suffix=None):
+    pkg_name = dep.cpp_info.get_property("make_name") or _get_package_reference_name(dep)
+    suffix = _get_suffix(dep, build_context_suffix)
+    return f"{pkg_name}{suffix}"
+
+
+def _get_component_name(dep, comp_name, build_context_suffix=None):
+    if comp_name not in dep.cpp_info.components:
+        # foo::foo might be referencing the root cppinfo
+        if _get_package_reference_name(dep) == comp_name:
+            return _get_package_name(dep, build_context_suffix)
+        raise ConanException("Component '{name}::{cname}' not found in '{name}' "
+                             "package requirement".format(name=_get_package_reference_name(dep),
+                                                          cname=comp_name))
+    comp_name = dep.cpp_info.components[comp_name].get_property("make_name")
+    suffix = _get_suffix(dep, build_context_suffix)
+    return f"{comp_name}{suffix}" if comp_name else None
+
+
+def _get_suffix(req, build_context_suffix=None):
+    """
+    Get the package name suffix coming from MakeDeps.build_context_suffix attribute, but only
+    for requirements declared as build requirement.
+
+    :param req: requirement ConanFile instance
+    :param build_context_suffix: `dict` with all the suffixes
+    :return: `str` with the suffix
+    """
+    if not build_context_suffix or not req.is_build_context:
+        return ""
+    return build_context_suffix.get(req.ref.name, "")
+
+
+def _get_formatted_dirs(folders, prefix_path_, name):
+    """ Format the directories to be used in the makefile, adding the prefix path if needed
+    :param folders: list of directories
+    :param prefix_path_: prefix path
+    :param name: package name
+    :return: list of formatted directories
+    """
+    ret = []
+    for i, directory in enumerate(folders):
+        directory = os.path.normpath(directory).replace("\\", "/")
+        prefix = ""
+        if not os.path.isabs(directory):
+            prefix = f"$(CONAN_ROOT_{name})/"
+        elif directory.startswith(prefix_path_):
+            prefix = f"$(CONAN_ROOT_{name})/"
+            directory = os.path.relpath(directory, prefix_path_).replace("\\", "/")
+        ret.append(f"{prefix}{directory}")
+    return ret
+
+
+# Makefile content information
+_MakeInfo = namedtuple("MakeInfo", ['name', 'requires', 'description', 'cpp_info', 'aliases'])
+
+# General makefile variables
+_cppinfo_variables = ['ROOT', 'SYSROOT', 'INCLUDE_DIRS', 'LIB_DIRS', 'BIN_DIRS', 'BUILD_DIRS',
+                      'RES_DIRS', 'LIBS', 'SYSTEM_LIBS', 'DEFINES', 'CFLAGS', 'CXXFLAGS',
+                      'SHAREDLINKFLAGS', 'EXELINKFLAGS', 'FRAMEWORKS', 'FRAMEWORK_PATHS']
+
+
+class _MakeContentGenerator(object):
+    """
+    Generate the makefile content for a given package and its components
+    """
+
+    def __init__(self, conanfile, dep):
+        self._conanfile = conanfile
+        self._dep = dep
+
+    # INFO: Need to sort the components to avoid duplicated variables
+    package_template = textwrap.dedent("""\
+            # Automatic Conan-generated file, do not edit.
+            # {{ raw_name }} makefile variables
+
+            {%- macro list_variables(packages, variable) -%}
+            {% set tab = "\t" %}
+            CONAN_{{variable}}_{{ name }} +=  $(sort \\
+            {%+ for package in packages[:-1] -%}
+            {{ tab }}$(CONAN_{{variable}}_{{package}}) \\
+            {%+ endfor -%}
+            {{ tab }}$(CONAN_{{variable}}_{{ packages[-1] }}) )
+            {%+ endmacro +%}
+
+            CONAN_DESCRIPTION_{{ name }} ?= {{ description }}
+            CONAN_VERSION_{{ name }} ?= {{ version }}
+
+            {% if requires|length %}
+            # {{ raw_name }} dependencies
+            include {{ requires|join(".mk ") }}.mk
+            {% endif %}
+
+            {% for variable in cppinfo_variables %}
+            {{ list_variables(makeifyed_requires, variable) }}
+            {% endfor %}
+    """)
+
+    component_template = textwrap.dedent("""\
+            # Automatic Conan-generated file, do not edit.
+            # {{ raw_name }} makefile variables
+
+            {%- macro list_variables(variables) -%}
+            {% set tab = "\t" %}
+            {%+ for var, value in variables.items() +%}
+            {% if value is iterable and value is not string %}
+            {{ var }} += \\
+            {%+ for it in value[:-1] -%}
+            {{ tab }}{{ it }} \\
+            {%+ endfor -%}
+            {{ tab }}{{ value[-1] }}
+            {% else %}
+            {{ var }} += {{ value }}
+            {% endif %}
+            {%+ endfor +%}
+            {%+ endmacro +%}
+
+            {% if requires|length %}
+            # {{ raw_name }} dependencies
+            include {{ requires|join(".mk ") }}.mk
+            {% endif %}
+
+            {{ list_variables(variables) }}
+    """)
+
+    def _makefy(self, name):
+        """ Parse a name to make it a valid makefile variable name
+        :param name: package or component name to be parsed
+        :return: parsed name
+        """
+        return re.sub(r'[^0-9A-Z_]', '_', name.upper())
+
+    def _var_ref(self, var):
+        return f"$({var})"
+
+    def content(self, info):
+        """ Generate the makefile content for a given package component
+        :param info: namedtuple with the package component information
+        :return: a makefile content
+        """
+        assert isinstance(info, _MakeInfo) and info.cpp_info is not None
+
+        var_aggregates = {}
+        variables = {}
+
+        def _rootify(root, root_id, path_list):
+            """
+            Replace common prefixes in a path list with a variable reference
+            """
+            root_len = len(root)
+            root_with_sep = root + os.path.sep
+            root_var_ref = self._var_ref(f"CONAN_ROOT_{root_id}")
+            return [root_var_ref + path[root_len:] \
+                    if path.startswith(root_with_sep) else path for path in path_list]
+
+        def _output_var(name, item, root=None, root_id=None, prefix_var=None):
+            """ Output a variable assignment, appropriately formatted
+            :param name: makefile variable name
+            :param item: makefile variable value
+            :param root: root path to be replaced with a variable reference
+            :param root_id: root variable id
+            :param prefix_var: variable to be prepended to the value
+            """
+            if item is None:
+                return False
+            if type(item).__name__ in ('Component', 'SettingsItem', 'Version', 'str'):
+                value = str(item)
+                if not value:
+                    return False
+                variables[name] = value
+            else:
+                assert type(item) is list
+                ritem = _rootify(root, root_id, item) if root else item
+                if prefix_var:
+                    ritem = [self._var_ref(prefix_var) + i for i in ritem]
+                if not ritem:
+                    return False
+                elif len(ritem) == 1:
+                    variables[name] = ritem[0]
+                else:
+                    variables[name] = ritem
+            return True
+
+        def _output_cpp_info_vars(var_id, cpp_info, root=None, root_id=None, add_to_aggregates=True):
+            """ Output the makefile variables for a given cpp_info
+            :param var_id: component name
+            :param cpp_info: cpp_info to provide component information
+            :param root: root path to be replaced with a variable reference
+            :param root_id: root variable id
+            :param add_to_aggregates: whether to add the variable to the aggregates
+            """
+            root_folder = self._dep.recipe_folder if self._dep.package_folder is None else self._dep.package_folder
+            prefix_path = root_folder.replace("\\", "/")
+
+            sysroot = cpp_info.sysroot if isinstance(cpp_info.sysroot, list) else [cpp_info.sysroot]
+            sysroot = _get_formatted_dirs(sysroot, prefix_path, self._makefy(info.name))
+
+            def _var(name, item, prefix_var=None):
+                v = f"CONAN_{name}_{var_id}"
+                if _output_var(v, item, root, root_id, prefix_var) and add_to_aggregates:
+                    var_aggregates.setdefault(name, []).append(self._var_ref(v))
+
+            _var("ROOT",            prefix_path, None)
+            _var("SYSROOT",         sysroot, None)
+
+            for var in ['INCLUDE_DIRS', 'LIB_DIRS', 'BIN_DIRS', 'SRC_DIRS', 'BUILD_DIRS', 'RES_DIRS', 'FRAMEWORK_DIRS']:
+                cppinfo_value = eval(f"cpp_info.{var.replace('_', '').lower()}")
+                dirs = _get_formatted_dirs(cppinfo_value, prefix_path, self._makefy(info.name))
+                _var(var, dirs, f"CONAN_{var}_FLAG")
+
+            _var("OBJECTS",         cpp_info.objects)
+            _var("LIBS",            cpp_info.libs,        "CONAN_LIB_FLAG")
+            _var("SYSTEM_LIBS",     cpp_info.system_libs, "CONAN_SYSTEM_LIB_FLAG")
+            _var("DEFINES",         cpp_info.defines,     "CONAN_DEFINE_FLAG")
+            _var("CFLAGS",          [var.replace('"', '\\"') for var in cpp_info.cflags])
+            _var("CXXFLAGS",        [var.replace('"', '\\"') for var in cpp_info.cxxflags])
+            _var("SHAREDLINKFLAGS", [var.replace('"', '\\"') for var in cpp_info.sharedlinkflags])
+            _var("EXELINKFLAGS",    [var.replace('"', '\\"') for var in cpp_info.exelinkflags])
+            _var("FRAMEWORKS",      cpp_info.frameworks)
+            _var("REQUIRES",        cpp_info.requires)
+
+        dep_name_list = []
+
+        dep_id = self._makefy(info.name)
+        dep_name_list.append(info.name)
+
+        root = self._dep.recipe_folder if self._dep.package_folder is None \
+            else self._dep.package_folder
+
+        comp_name_list = []
+
+        comp_id = self._makefy(info.name)
+        comp_name_list.append(info.name)
+        _output_cpp_info_vars(comp_id, info.cpp_info, root, dep_id, False)
+
+        for var_name, var_list in var_aggregates.items():
+            _output_var(f"CONAN_{var_name}", var_list)
+
+        return self.component_content(info, variables)
+
+    def package_content(self, info):
+        """ Generate the makefile content for the package, based on jinja2 template
+        :param info: _MakeInfo instance
+        :return: makefile content
+        """
+        assert isinstance(info, _MakeInfo)
+
+        context = {
+            "name": self._makefy(info.name),
+            "raw_name": info.name,
+            "description": info.description,
+            "version": self._dep.ref.version,
+            "requires": info.requires,
+            "makeifyed_requires": [self._makefy(r) for r in info.requires],
+            "cppinfo_variables": _cppinfo_variables
+        }
+        template = Template(self.package_template, trim_blocks=True,
+                            lstrip_blocks=True, undefined=StrictUndefined)
+        return template.render(context)
+
+    def component_content(self, info, variables):
+        """ Generate the makefile content for the package component, based on jinja2 template
+        :param info: _MakeInfo instance
+        :return: makefile content
+        """
+        assert isinstance(info, _MakeInfo)
+
+        context = {
+            "name": self._makefy(info.name),
+            "raw_name": info.name,
+            "description": info.description,
+            "version": self._dep.ref.version,
+            "requires": info.requires,
+            "makeifyed_requires": [self._makefy(r) for r in info.requires],
+            "variables": variables,
+        }
+        template = Template(self.component_template, trim_blocks=True,
+                            lstrip_blocks=True, undefined=StrictUndefined)
+        return template.render(context)
+
+
+class _MakeGlobalContentGenerator(_MakeContentGenerator):
path: conan/tools/gnu/makedeps.py
text: @user1:
I'd prefer to avoid inheritance, and use composition, or reuse of free functions.

diff_hunk: @@ -0,0 +1,602 @@
+import os
+import re
+import textwrap
+
+from collections import namedtuple
+from jinja2 import Template, StrictUndefined
+
+
+from conan.internal import check_duplicated_generator
+from conan.tools.files import save
+from conans.errors import ConanException
+from conans.model.dependencies import get_transitive_requires
+
+
+def _get_name_with_namespace(namespace, name):
+    """ Build a name with a namespace, e.g., openssl-crypto
+    :param namespace: package name
+    :param name: component name
+    :return: name with namespace
+    """
+    if namespace == name or name.startswith(namespace):
+        return name
+    return f"{namespace}-{name}"
+
+
+def _get_package_reference_name(dep):
+    """ Get the reference name for the given package
+    :param dep: package reference instance
+    :return: reference name
+    """
+    return dep.ref.name
+
+
+def _get_package_aliases(dep):
+    pkg_aliases = dep.cpp_info.get_property("make_aliases")
+    return pkg_aliases or []
+
+
+def _get_component_aliases(dep, comp_name):
+    if comp_name not in dep.cpp_info.components:
+        # foo::foo might be referencing the root cppinfo
+        if _get_package_reference_name(dep) == comp_name:
+            return _get_package_aliases(dep)
+        raise ConanException("Component '{name}::{cname}' not found in '{name}' "
+                             "package requirement".format(name=_get_package_reference_name(dep),
+                                                          cname=comp_name))
+    comp_aliases = dep.cpp_info.components[comp_name].get_property("make_aliases")
+    return comp_aliases or []
+
+
+def _get_package_name(dep, build_context_suffix=None):
+    pkg_name = dep.cpp_info.get_property("make_name") or _get_package_reference_name(dep)
+    suffix = _get_suffix(dep, build_context_suffix)
+    return f"{pkg_name}{suffix}"
+
+
+def _get_component_name(dep, comp_name, build_context_suffix=None):
+    if comp_name not in dep.cpp_info.components:
+        # foo::foo might be referencing the root cppinfo
+        if _get_package_reference_name(dep) == comp_name:
+            return _get_package_name(dep, build_context_suffix)
+        raise ConanException("Component '{name}::{cname}' not found in '{name}' "
+                             "package requirement".format(name=_get_package_reference_name(dep),
+                                                          cname=comp_name))
+    comp_name = dep.cpp_info.components[comp_name].get_property("make_name")
+    suffix = _get_suffix(dep, build_context_suffix)
+    return f"{comp_name}{suffix}" if comp_name else None
+
+
+def _get_suffix(req, build_context_suffix=None):
+    """
+    Get the package name suffix coming from MakeDeps.build_context_suffix attribute, but only
+    for requirements declared as build requirement.
+
+    :param req: requirement ConanFile instance
+    :param build_context_suffix: `dict` with all the suffixes
+    :return: `str` with the suffix
+    """
+    if not build_context_suffix or not req.is_build_context:
+        return ""
+    return build_context_suffix.get(req.ref.name, "")
+
+
+def _get_formatted_dirs(folders, prefix_path_, name):
+    """ Format the directories to be used in the makefile, adding the prefix path if needed
+    :param folders: list of directories
+    :param prefix_path_: prefix path
+    :param name: package name
+    :return: list of formatted directories
+    """
+    ret = []
+    for i, directory in enumerate(folders):
+        directory = os.path.normpath(directory).replace("\\", "/")
+        prefix = ""
+        if not os.path.isabs(directory):
+            prefix = f"$(CONAN_ROOT_{name})/"
+        elif directory.startswith(prefix_path_):
+            prefix = f"$(CONAN_ROOT_{name})/"
+            directory = os.path.relpath(directory, prefix_path_).replace("\\", "/")
+        ret.append(f"{prefix}{directory}")
+    return ret
+
+
+# Makefile content information
+_MakeInfo = namedtuple("MakeInfo", ['name', 'requires', 'description', 'cpp_info', 'aliases'])
+
+# General makefile variables
+_cppinfo_variables = ['ROOT', 'SYSROOT', 'INCLUDE_DIRS', 'LIB_DIRS', 'BIN_DIRS', 'BUILD_DIRS',
+                      'RES_DIRS', 'LIBS', 'SYSTEM_LIBS', 'DEFINES', 'CFLAGS', 'CXXFLAGS',
+                      'SHAREDLINKFLAGS', 'EXELINKFLAGS', 'FRAMEWORKS', 'FRAMEWORK_PATHS']
+
+
+class _MakeContentGenerator(object):
+    """
+    Generate the makefile content for a given package and its components
+    """
+
+    def __init__(self, conanfile, dep):
+        self._conanfile = conanfile
+        self._dep = dep
+
+    # INFO: Need to sort the components to avoid duplicated variables
+    package_template = textwrap.dedent("""\
+            # Automatic Conan-generated file, do not edit.
+            # {{ raw_name }} makefile variables
+
+            {%- macro list_variables(packages, variable) -%}
+            {% set tab = "\t" %}
+            CONAN_{{variable}}_{{ name }} +=  $(sort \\
+            {%+ for package in packages[:-1] -%}
+            {{ tab }}$(CONAN_{{variable}}_{{package}}) \\
+            {%+ endfor -%}
+            {{ tab }}$(CONAN_{{variable}}_{{ packages[-1] }}) )
+            {%+ endmacro +%}
+
+            CONAN_DESCRIPTION_{{ name }} ?= {{ description }}
+            CONAN_VERSION_{{ name }} ?= {{ version }}
+
+            {% if requires|length %}
+            # {{ raw_name }} dependencies
+            include {{ requires|join(".mk ") }}.mk
+            {% endif %}
+
+            {% for variable in cppinfo_variables %}
+            {{ list_variables(makeifyed_requires, variable) }}
+            {% endfor %}
+    """)
+
+    component_template = textwrap.dedent("""\
+            # Automatic Conan-generated file, do not edit.
+            # {{ raw_name }} makefile variables
+
+            {%- macro list_variables(variables) -%}
+            {% set tab = "\t" %}
+            {%+ for var, value in variables.items() +%}
+            {% if value is iterable and value is not string %}
+            {{ var }} += \\
+            {%+ for it in value[:-1] -%}
+            {{ tab }}{{ it }} \\
+            {%+ endfor -%}
+            {{ tab }}{{ value[-1] }}
+            {% else %}
+            {{ var }} += {{ value }}
+            {% endif %}
+            {%+ endfor +%}
+            {%+ endmacro +%}
+
+            {% if requires|length %}
+            # {{ raw_name }} dependencies
+            include {{ requires|join(".mk ") }}.mk
+            {% endif %}
+
+            {{ list_variables(variables) }}
+    """)
+
+    def _makefy(self, name):
+        """ Parse a name to make it a valid makefile variable name
+        :param name: package or component name to be parsed
+        :return: parsed name
+        """
+        return re.sub(r'[^0-9A-Z_]', '_', name.upper())
+
+    def _var_ref(self, var):
+        return f"$({var})"
+
+    def content(self, info):
+        """ Generate the makefile content for a given package component
+        :param info: namedtuple with the package component information
+        :return: a makefile content
+        """
+        assert isinstance(info, _MakeInfo) and info.cpp_info is not None
+
+        var_aggregates = {}
+        variables = {}
+
+        def _rootify(root, root_id, path_list):
+            """
+            Replace common prefixes in a path list with a variable reference
+            """
+            root_len = len(root)
+            root_with_sep = root + os.path.sep
+            root_var_ref = self._var_ref(f"CONAN_ROOT_{root_id}")
+            return [root_var_ref + path[root_len:] \
+                    if path.startswith(root_with_sep) else path for path in path_list]
+
+        def _output_var(name, item, root=None, root_id=None, prefix_var=None):
+            """ Output a variable assignment, appropriately formatted
+            :param name: makefile variable name
+            :param item: makefile variable value
+            :param root: root path to be replaced with a variable reference
+            :param root_id: root variable id
+            :param prefix_var: variable to be prepended to the value
+            """
+            if item is None:
+                return False
+            if type(item).__name__ in ('Component', 'SettingsItem', 'Version', 'str'):
+                value = str(item)
+                if not value:
+                    return False
+                variables[name] = value
+            else:
+                assert type(item) is list
+                ritem = _rootify(root, root_id, item) if root else item
+                if prefix_var:
+                    ritem = [self._var_ref(prefix_var) + i for i in ritem]
+                if not ritem:
+                    return False
+                elif len(ritem) == 1:
+                    variables[name] = ritem[0]
+                else:
+                    variables[name] = ritem
+            return True
+
+        def _output_cpp_info_vars(var_id, cpp_info, root=None, root_id=None, add_to_aggregates=True):
+            """ Output the makefile variables for a given cpp_info
+            :param var_id: component name
+            :param cpp_info: cpp_info to provide component information
+            :param root: root path to be replaced with a variable reference
+            :param root_id: root variable id
+            :param add_to_aggregates: whether to add the variable to the aggregates
+            """
+            root_folder = self._dep.recipe_folder if self._dep.package_folder is None else self._dep.package_folder
+            prefix_path = root_folder.replace("\\", "/")
+
+            sysroot = cpp_info.sysroot if isinstance(cpp_info.sysroot, list) else [cpp_info.sysroot]
+            sysroot = _get_formatted_dirs(sysroot, prefix_path, self._makefy(info.name))
+
+            def _var(name, item, prefix_var=None):
+                v = f"CONAN_{name}_{var_id}"
+                if _output_var(v, item, root, root_id, prefix_var) and add_to_aggregates:
+                    var_aggregates.setdefault(name, []).append(self._var_ref(v))
+
+            _var("ROOT",            prefix_path, None)
+            _var("SYSROOT",         sysroot, None)
+
+            for var in ['INCLUDE_DIRS', 'LIB_DIRS', 'BIN_DIRS', 'SRC_DIRS', 'BUILD_DIRS', 'RES_DIRS', 'FRAMEWORK_DIRS']:
+                cppinfo_value = eval(f"cpp_info.{var.replace('_', '').lower()}")
+                dirs = _get_formatted_dirs(cppinfo_value, prefix_path, self._makefy(info.name))
+                _var(var, dirs, f"CONAN_{var}_FLAG")
+
+            _var("OBJECTS",         cpp_info.objects)
+            _var("LIBS",            cpp_info.libs,        "CONAN_LIB_FLAG")
+            _var("SYSTEM_LIBS",     cpp_info.system_libs, "CONAN_SYSTEM_LIB_FLAG")
+            _var("DEFINES",         cpp_info.defines,     "CONAN_DEFINE_FLAG")
+            _var("CFLAGS",          [var.replace('"', '\\"') for var in cpp_info.cflags])
+            _var("CXXFLAGS",        [var.replace('"', '\\"') for var in cpp_info.cxxflags])
+            _var("SHAREDLINKFLAGS", [var.replace('"', '\\"') for var in cpp_info.sharedlinkflags])
+            _var("EXELINKFLAGS",    [var.replace('"', '\\"') for var in cpp_info.exelinkflags])
+            _var("FRAMEWORKS",      cpp_info.frameworks)
+            _var("REQUIRES",        cpp_info.requires)
+
+        dep_name_list = []
+
+        dep_id = self._makefy(info.name)
+        dep_name_list.append(info.name)
+
+        root = self._dep.recipe_folder if self._dep.package_folder is None \
+            else self._dep.package_folder
+
+        comp_name_list = []
+
+        comp_id = self._makefy(info.name)
+        comp_name_list.append(info.name)
+        _output_cpp_info_vars(comp_id, info.cpp_info, root, dep_id, False)
+
+        for var_name, var_list in var_aggregates.items():
+            _output_var(f"CONAN_{var_name}", var_list)
+
+        return self.component_content(info, variables)
+
+    def package_content(self, info):
+        """ Generate the makefile content for the package, based on jinja2 template
+        :param info: _MakeInfo instance
+        :return: makefile content
+        """
+        assert isinstance(info, _MakeInfo)
+
+        context = {
+            "name": self._makefy(info.name),
+            "raw_name": info.name,
+            "description": info.description,
+            "version": self._dep.ref.version,
+            "requires": info.requires,
+            "makeifyed_requires": [self._makefy(r) for r in info.requires],
+            "cppinfo_variables": _cppinfo_variables
+        }
+        template = Template(self.package_template, trim_blocks=True,
+                            lstrip_blocks=True, undefined=StrictUndefined)
+        return template.render(context)
+
+    def component_content(self, info, variables):
+        """ Generate the makefile content for the package component, based on jinja2 template
+        :param info: _MakeInfo instance
+        :return: makefile content
+        """
+        assert isinstance(info, _MakeInfo)
+
+        context = {
+            "name": self._makefy(info.name),
+            "raw_name": info.name,
+            "description": info.description,
+            "version": self._dep.ref.version,
+            "requires": info.requires,
+            "makeifyed_requires": [self._makefy(r) for r in info.requires],
+            "variables": variables,
+        }
+        template = Template(self.component_template, trim_blocks=True,
+                            lstrip_blocks=True, undefined=StrictUndefined)
+        return template.render(context)
+
+
+class _MakeGlobalContentGenerator(_MakeContentGenerator):
+    """
+    Generates the content of the conandeps.mk file
+    """
+
+    # INFO: Need to sort the components to avoid duplicated variables
+    global_template = textwrap.dedent("""\
+            # Automatic Conan-generated file, do not edit.
+
+            {%- macro list_variable(packages, variable) -%}
+            {% set tab = "\t" %}
+            CONAN_{{variable}} += $(sort \\
+            {%+ for package in packages[:-1] -%}
+            {{ tab }}$(CONAN_{{variable}}_{{package}}) \\
+            {%+ endfor -%}
+            {{ tab }}$(CONAN_{{variable}}_{{packages[-1]}}) )
+            {%+ endmacro +%}
+
+            # Settings variables
+            {% for key, value in host_settings.items() %}
+            {{ key }} ?= {{ value }}
+            {% endfor %}
+
+            {% for key, value in build_settings.items() %}
+            {{ key }} ?= {{ value }}
+            {% endfor %}
+
+            # Global dependencies
+            include {{ makefiles|join(".mk ") }}.mk
+
+            # Global variables
+            {% for variable in cppinfo_variables %}
+            {{ list_variable(folders, variable) }}
+            {% endfor %}
+    """)
+
+    def __init__(self, conanfile, dep):
+        super().__init__(conanfile, dep)
+        self._conanfile = conanfile
+
+    def content(self, info):
+        """ Generate the content of the conandeps.mk file
+        :param info: dict with the dependencies list
+        """
+        requires_names = [_get_package_name(req) for req in info.values()]
+        build_settings = {"CONAN_BUILD_" + self._makefy(key): value for key, value in self._conanfile.settings_build.items()}
+        host_settings = {"CONAN_HOST_" + self._makefy(key): value for key, value in self._conanfile.settings.items()}
+        folders = []
+        for deps in info.values():
+            folders.append(self._makefy(deps.ref.name))
+
+        context = {
+            "build_settings": build_settings,
+            "host_settings": host_settings,
+            "folders": folders,
+            "makefiles": requires_names,
+            "cppinfo_variables": _cppinfo_variables
+        }
+        template = Template(self.global_template, trim_blocks=True, lstrip_blocks=True, undefined=StrictUndefined)
+        return template.render(context)
+
+
+class _MakeGenerator:
+    """
+    Generates makefiles for all components and their root component
+    """
+
+    def __init__(self, conanfile, dep, build_context_suffix=None):
+        """
+        :param conanfile: Conanfile instance
+        :param dep: conans.model.ConanFileReference instance
+        :param build_context_suffix: dict with the suffixes for the build context
+        """
+        self._conanfile = conanfile
+        self._build_context_suffix = build_context_suffix or {}
+        self._dep = dep
+        self._content_generator = _MakeContentGenerator(self._conanfile, self._dep)
+        self._transitive_reqs = get_transitive_requires(self._conanfile, dep)
+
+    def _get_cpp_info_requires_names(self, cpp_info):
+        """ Get all the makefile valid names from the requires ones given a CppInfo object.
+        :param cpp_info: CppInfo object
+        :return: list of components names
+        """
+        dep_ref_name = _get_package_reference_name(self._dep)
+        ret = []
+        for req in cpp_info.requires:
+            pkg_ref_name, comp_ref_name = req.split("::") if "::" in req else (dep_ref_name, req)
+            # For instance, dep == "hello/1.0" and req == "other::cmp1" -> hello != other
+            if dep_ref_name != pkg_ref_name:
+                try:
+                    req_conanfile = self._transitive_reqs[pkg_ref_name]
+                except KeyError:
+                    continue  # If the dependency is not in the transitive, might be skipped
+            else:  # For instance, dep == "hello/1.0" and req == "hello::cmp1" -> hello == hello
+                req_conanfile = self._dep
+            comp_name = _get_component_name(req_conanfile, comp_ref_name, self._build_context_suffix)
+            if not comp_name:
+                pkg_name = _get_package_name(req_conanfile, self._build_context_suffix)
+                # Creating a component name with namespace, e.g., dep-comp1
+                comp_name = _get_name_with_namespace(pkg_name, comp_ref_name)
+            ret.append(comp_name)
+        return ret
+
+    @property
+    def components_info(self):
+        """
+        Get the whole package and its components information like their own requires, names and even
+        the cpp_info for each component.
+
+        :return: `list` of `_MakeInfo` objects with all the components information
+        """
+        pkg_name = _get_package_name(self._dep, self._build_context_suffix)
+        components_info = []
+        # Loop through all the package's components
+        for comp_ref_name, cpp_info in self._dep.cpp_info.get_sorted_components().items():
+            # At first, let's check if we have defined some components requires, e.g., "dep::cmp1"
+            comp_requires_names = self._get_cpp_info_requires_names(cpp_info)
+            comp_name = _get_component_name(self._dep, comp_ref_name, self._build_context_suffix)
+            if not comp_name:
+                comp_name = _get_name_with_namespace(pkg_name, comp_ref_name)
+                comp_description = f"Conan component: {comp_name}"
+            else:
+                comp_description = f"Conan component: {pkg_name}-{comp_name}"
+            comp_aliases = _get_component_aliases(self._dep, comp_ref_name)
+            # Save each component information
+            components_info.append(_MakeInfo(comp_name, comp_requires_names, comp_description,
+                                           cpp_info, comp_aliases))
+        return components_info
+
+    @property
+    def package_info(self):
+        """
+        Get the whole package information
+
+        :return: `_MakeInfo` object with the package information
+        """
+        pkg_name = _get_package_name(self._dep, self._build_context_suffix)
+        # At first, let's check if we have defined some global requires, e.g., "other::cmp1"
+        requires = self._get_cpp_info_requires_names(self._dep.cpp_info)
+        # If we have found some component requires it would be enough
+        if not requires:
+            # If no requires were found, let's try to get all the direct dependencies,
+            # e.g., requires = "other_pkg/1.0"
+            requires = [_get_package_name(req, self._build_context_suffix)
+                        for req in self._transitive_reqs.values()]
+        description = "Conan package: %s" % pkg_name
+        aliases = _get_package_aliases(self._dep)
+        cpp_info = self._dep.cpp_info
+        return _MakeInfo(pkg_name, requires, description, cpp_info, aliases)
+
+    @property
+    def make_files(self):
+        """
+        Get all the Makfiles and contents for any dependency:
+
+        * If the given dependency does not have components:
+            The makefile will be the dependency one.
+
+        * If the given dependency has components:
+            The .mk files will be saved in this order:
+                1- Package components.
+                2- Root component.
+
+            Note: If the root-package Makefile name matches with any other of the components one, the first one
+            is not going to be created. Components have more priority than root package.
+
+        * Apart from those makefiles, if there are any aliases declared, they will be created too.
+        """
+        def _update_make_files(info):
+            make_files[f"{info.name}.mk"] = self._content_generator.content(info)
+            for alias in info.aliases:
+                alias_info = _MakeInfo(alias, [info.name], f"Alias {alias} for {info.name}", None, [])
+                make_files[f"{alias}.mk"] = self._content_generator.package_content(alias_info)
+
+        make_files = {}
+        # If the package has no components, then we have to calculate only the root makefile
+        if not self._dep.cpp_info.has_components:
+            package_info = self.package_info
+            _update_make_files(package_info)
+            return make_files
+
+        # First, let's load all the components makefile files
+        # Loop through all the package's components
+        pkg_requires = []
+        for component_info in self.components_info:
+            _update_make_files(component_info)
+            # Saving components name as the package requires
+            pkg_requires.append(component_info.name)
+
+        # Second, let's load the root package's makefile file ONLY
+        # if it does not already exist in components one
+        pkg_name = _get_package_name(self._dep, self._build_context_suffix)
+        if f"{pkg_name}.mk" not in make_files:
+            package_info = _MakeInfo(pkg_name, pkg_requires, f"Conan package: {pkg_name}", None, _get_package_aliases(self._dep))
+            # It'll be enough creating a makefile. This file will be like an alias
+            make_files[f"{package_info.name}.mk"] = self._content_generator.package_content(package_info)
+            for alias in package_info.aliases:
+                alias_info = _MakeInfo(alias, [package_info.name], f"Alias {alias} for {package_info.name}", None, [])
+                make_files[f"{alias}.mk"] = self._content_generator.package_content(alias_info)
+
+        return make_files
+
+
+class _MakeGlobalGenerator:
+    """
+    Generate and stare the conandeps.mk file
+    """
+
+    _output_filename = "conandeps.mk"
+
+    def __init__(self, conanfile, dep, reqs):
+        """
+        :param conanfile: The current recipe object
+        :param dep: The current dependency object
+        :param reqs: The current dependency requirements
+        """
+        self._content_global_generator = _MakeGlobalContentGenerator(conanfile, dep)
+        self._reqs = reqs
+
+    @property
+    def make_file(self):
+        """
+        Get the .mk file name and its content
+        """
+        return {self._output_filename: self._content_global_generator.content(self._reqs)}
+
+
+class MakeDeps(object):
+    """
+    Generate and store all the .mk files for all the dependencies
+    """
+
+    def __init__(self, conanfile):
+        """
+        :param conanfile: The current recipe object
+        """
+        self._conanfile = conanfile
+        self.build_context_activated = []
+        self.build_context_suffix = {}
path: conan/tools/gnu/makedeps.py
text: @user1:
We can drop the build context at the moment, lets start small and simpler with host context first

@author:
Okay, makes sense. Most of this generator I made based on pkg-config deps, that's a very complete generator, but I forgot a very important information, pkg-config is consumed by autotools, but make deps is something totally separated.

diff_hunk: @@ -0,0 +1,213 @@
+import os.path
+import platform
+import re
+import textwrap
+
+import pytest
+
+from conans.test.assets.genconanfile import GenConanfile
+from conans.test.utils.tools import TestClient
+
+
+@pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Make")
+def test_link_lib_correct_order():
+    client = TestClient()
+    liba = GenConanfile().with_name("liba").with_version("0.1")
+    libb = GenConanfile().with_name("libb").with_version("0.1").with_require("liba/0.1")
+    libc = GenConanfile().with_name("libc").with_version("0.1").with_require("libb/0.1")
+    consumer = GenConanfile().with_require("libc/0.1")
+    client.save({"liba.py": liba, "libb.py": libb, "libc.py": libc, "consumer.py": consumer})
+    client.run("create liba.py")
+    pid_a = client.created_package_reference("liba/0.1")
+    folder_a = client.get_latest_pkg_layout(pid_a).package()
+    client.run("create libb.py")
+    pid_b = client.created_package_reference("libb/0.1")
+    folder_b = client.get_latest_pkg_layout(pid_b).package()
+    client.run("create libc.py")
+    pid_c = client.created_package_reference("libc/0.1")
+    folder_c = client.get_latest_pkg_layout(pid_c).package()
+    client.run("install consumer.py -g MakeDeps")
+    deps = client.load("conanautotoolsdeps.sh")
+    # check the libs are added in the correct order with this regex
+    assert re.search("export LDFLAGS.*{}.*{}.*{}".format(folder_c, folder_b, folder_a), deps)
+
+
+@pytest.mark.skipif(platform.system() not in ["Linux", "Darwin"], reason="Make")
+def test_cpp_info_aggregation():
+
+    profile = textwrap.dedent("""
+         [settings]
+         build_type=Release
+         arch=x86
+         os=Macos
+         compiler=gcc
+         compiler.libcxx=libstdc++11
+         compiler.version=7.1
+         compiler.cppstd=17
+    """)
+
+    dep_conanfile = textwrap.dedent("""
+
+    from conan import ConanFile
+
+    class Dep(ConanFile):
+
+        settings = "os", "arch", "compiler", "build_type"
+
+        def package_info(self):
+            self.cpp_info.includedirs = []
+            self.cpp_info.includedirs.append("path/includes/{}".format(self.name))
+            self.cpp_info.includedirs.append("other\\include\\path\\{}".format(self.name))
path: conans/test/integration/toolchains/gnu/test_makedeps.py
text: @user1:
If test not Windows, why using \\ backslash in the path?

@author:
this test is invalid, I'll remove it. I would prefer not supporting Windows with MakeDeps for now, we have NMake already for windows. 

@user1:
Yes, make sense, you can start with support for Linux/OSX only, and we can bring Windows (via msys2) later