conan-io/conan

View on GitHub
conans/client/generators/markdown.py

Summary

Maintainability
A
0 mins
Test Coverage
import datetime
import os
import textwrap

from jinja2 import DictLoader
from jinja2 import Environment

from conan.tools.cmake.cmakedeps.cmakedeps import CMakeDeps
from conan.tools.cmake.cmakedeps.templates import CMakeDepsFileTemplate
from conan.tools.gnu.pkgconfigdeps import (
    _get_component_name as pkgconfig_get_component_name,
    _get_name_with_namespace as pkgconfig_get_name_with_namespace,
    _get_package_name as pkgconfig_get_package_name
)
from conans.model import Generator


requirement_tpl = textwrap.dedent("""
    {% from 'macros' import render_component_cpp_info %}

    # {{ requirement }}

    ---

    {% if requires %}

    <br>

    ## {{ requirement }} dependencies
    {% for dep_name, dep in requires %}
    * [{{ dep }}](https://conan.io/center/{{ dep_name }})
    {%- endfor -%}
    {%- endif %}

    <br>

    ## Using the {{ requirement.ref.name }} Conan Package

    <br>

    Conan integrates with different build systems. You can declare which build system you want your project to use setting in the **[generators]** section of the [conanfile.txt](https://docs.conan.io/en/latest/reference/conanfile_txt.html#generators) or using the **generators** attribute in the [conanfile.py](https://docs.conan.io/en/latest/reference/conanfile/attributes.html#generators). Here, there is some basic information you can use to integrate **{{ requirement.ref.name }}** in your own project. For more detailed information, please [check the Conan documentation](https://docs.conan.io/en/latest/getting_started.html).

    {% include 'buildsystem_cmake' %}
    {% include 'buildsystem_vs' %}
    {% include 'buildsystem_autotools' %}
    {% include 'buildsystem_other' %}
    {% include 'components' %}
    {% include 'headers' %}

    ---
    ---
    Conan **{{ conan_version }}**. JFrog LTD. [https://conan.io](https://conan.io). Autogenerated {{ now.strftime('%Y-%m-%d %H:%M:%S') }}.
""")

macros = textwrap.dedent("""
    {% macro join_list_code(items) -%}
    ``{{ "``, ``".join(items) }}``
    {%- endmacro %}

    {% macro join_list_bold(items) -%}
    **{{ "**, **".join(items) }}**
    {%- endmacro %}

    {% macro render_component_cpp_info(target_name, pc_name, cpp_info) %}
    * CMake target name: ``{{ target_name }}``
    * pkg-config *.pc* file: **{{ pc_name }}.pc**
    {%- if cpp_info.requires is iterable and cpp_info.requires %}
    * Requires other components: {{ join_list_bold(cpp_info.requires) }}
    {%- endif %}
    {%- if cpp_info.libs %}
    * Links to libraries: {{ join_list_bold(cpp_info.libs) }}
    {%- endif %}
    {%- if cpp_info.system_libs %}
    * Systems libs: {{ join_list_bold(cpp_info.system_libs) }}
    {%- endif %}
    {%- if cpp_info.defines %}
    * Preprocessor definitions: {{ join_list_code(cpp_info.defines) }}
    {%- endif %}
    {%- if cpp_info.cflags %}
    * C_FLAGS: {{ join_list_code(cpp_info.cflags) }}
    {%- endif %}
    {%- if cpp_info.cxxflags %}
    * CXX_FLAGS: {{ join_list_code(cpp_info.cxxflags) }}
    {%- endif %}
    {%- endmacro %}
""")

buildsystem_cmake_tpl = textwrap.dedent("""

    <br>

    ## Using {{ requirement.ref.name }} with CMake

    <br>

    ### [Conan CMake generators](https://docs.conan.io/en/latest/reference/conanfile/tools/cmake.html)

    <br>

    * [CMakeDeps](https://docs.conan.io/en/latest/reference/conanfile/tools/cmake/cmakedeps.html): generates information about where the **{{ requirement.ref.name }}** library and its dependencies {% if requires %} ({% for dep_name, dep in requires %} [{{ dep_name }}](https://conan.io/center/{{ dep_name }}){% if not loop.last %}, {% endif %} {%- endfor -%}) {%- endif %} are installed together with other information like version, flags, and directory data or configuration. CMake will use this files when you invoke ``find_package()`` in your *CMakeLists.txt*.

    * [CMakeToolchain](https://docs.conan.io/en/latest/reference/conanfile/tools/cmake/cmaketoolchain.html): generates a CMake toolchain file that you can later invoke with CMake in the command line using `-DCMAKE_TOOLCHAIN_FILE=conantoolchain.cmake`.

    Declare these generators in your **conanfile.txt** along with your **{{ requirement.ref.name }}** dependency like:

    ```ini
    [requires]
    {{ requirement }}

    [generators]
    CMakeDeps
    CMakeToolchain
    ```

    <br>

    To use **{{ requirement.ref.name }}** in a simple CMake project with this structure:

    ```shell
    .
    |-- CMakeLists.txt
    |-- conanfile.txt
    `-- src
        `-- main.{{ project_extension }}
    ```

    <br>

    Your **CMakeLists.txt** could look similar to this, using the global **{{ cmake_variables.global_target_name }}** CMake's target:

    ```cmake
    cmake_minimum_required(VERSION 3.15)
    project({{ requirement.ref.name }}_project {{ project_type }})

    find_package({{ cmake_variables.file_name }})

    add_executable(${PROJECT_NAME} src/main{{ project_extension }})

    # Use the global target
    target_link_libraries(${PROJECT_NAME} {{ cmake_variables.global_target_name }})
    ```

    <br>

    To install **{{ requirement }}**, its dependencies and build your project, you just have to do:

    ```shell
    # for Linux/macOS
    $ conan install . --install-folder cmake-build-release --build=missing
    $ cmake . -DCMAKE_TOOLCHAIN_FILE=cmake-build-release/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release
    $ cmake --build .

    # for Windows and Visual Studio 2017
    $ conan install . --output-folder cmake-build --build=missing
    $ cmake . -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE=cmake-build/conan_toolchain.cmake
    $ cmake --build . --config Release
    ```

    {% if requirement.cpp_info.has_components %}

    <br>

    {% for component_name, target_name in cmake_variables.component_alias.items() %}
    {%- if loop.index==1 %}

    As the {{ requirement.ref.name }} Conan package defines components, you can link only the desired parts of the library in your project. For example, linking only with the {{ requirement.ref.name }} **{{ component_name }}** component, through the **{{ target_name }}** target.

    ```cmake
    ...
    # Link just to {{ requirement.ref.name }} {{ component_name }} component
    target_link_libraries(${PROJECT_NAME} {{ target_name }})
    ```

    <br>

    To check all the available components for **{{ requirement.ref.name }}** Conan package, please check the dedicated section at the end of this document.

    {%- endif %}
    {%- endfor %}

    {%- endif %}

    {% set cmake_build_modules = requirement.cpp_info.get_property('cmake_build_modules') %}
    {% if cmake_build_modules %}

    <br>

    ### Declared CMake build modules

    <br>

    {% for bm in cmake_build_modules -%}
    #### {{ relpath(bm, package_folder) | replace("\\\\", "/") }}
      ```cmake
      {{ bm|read_pkg_file|indent(width=2) }}
      ```
    {%- endfor -%}
    {%- endif %}

""")

buildsystem_vs_tpl = textwrap.dedent("""

    <br>

    ## Using {{ requirement.ref.name }} with Visual Studio

    <br>

    ### [Visual Studio Conan generators](https://docs.conan.io/en/latest/reference/conanfile/tools/microsoft.html)

    <br>

    * [MSBuildDeps](https://docs.conan.io/en/latest/reference/conanfile/tools/microsoft.html#msbuilddeps): generates the **conandeps.props** properties file with information about where the **{{ requirement.ref.name }}** library and its dependencies {% if requires %} ({% for dep_name, dep in requires %} [{{ dep_name }}](https://conan.io/center/{{ dep_name }}){% if not loop.last %}, {% endif %} {%- endfor -%}) {%- endif %} are installed together with other information like version, flags, and directory data or configuration.

    * [MSBuildToolchain](https://docs.conan.io/en/latest/reference/conanfile/tools/microsoft.html#msbuildtoolchain): Generates the **conantoolchain.props** properties file with the current package configuration, settings, and options.

    Declare these generators in your **conanfile.txt** along with your **{{ requirement.ref.name }}** dependency like:

    ```ini
    [requires]
    {{ requirement }}

    [generators]
    MSBuildDeps
    MSBuildToolchain
    ```

    <br>

    Please, [check the Conan documentation](https://docs.conan.io/en/latest/reference/conanfile/tools/microsoft.html) for more detailed information on how to add these properties files to your Visual Studio projects.

""")

buildsystem_autotools_tpl = textwrap.dedent("""

    <br>

    ## Using {{ requirement.ref.name }} with Autotools and pkg-config

    <br>

    ### [Autotools Conan generators](https://docs.conan.io/en/latest/reference/conanfile/tools/gnu.html)

    <br>

    * [AutotoolsToolchain](https://docs.conan.io/en/latest/reference/conanfile/tools/gnu/autotoolstoolchain.html): generates the **conanautotoolstoolchain.sh/bat** script translating information from the current package configuration, settings, and options setting some enviroment variables for Autotools like: ``CPPFLAGS``, ``CXXFLAGS``, ``CFLAGS`` and ``LDFLAGS``. It will also generate a ``deactivate_conanautotoolstoolchain.sh/bat`` so you can restore your environment.

    * [AutotoolsDeps](https://docs.conan.io/en/latest/reference/conanfile/tools/gnu/autotoolsdeps.html): generates the **conanautotoolsdeps.sh/bat** script with information about where the **{{ requirement.ref.name }}** library and its dependencies {% if requires %} ({% for dep_name, dep in requires %} [{{ dep_name }}](https://conan.io/center/{{ dep_name }}){% if not loop.last %}, {% endif %} {%- endfor -%}) {%- endif %} are installed together with other information like version, flags, and directory data or configuration. This is done setting some enviroment variables for Autotools like: ``LIBS``, ``CPPFLAGS``,``CXXFLAGS``, ``CFLAGS`` and ``LDFLAGS``.  It will also generate a ``deactivate_conanautotoolsdeps.sh/bat`` so you can restore your environment.

    Declare these generators in your **conanfile.txt** along with your **{{ requirement.ref.name }}** dependency like:

    ```ini
    [requires]
    {{ requirement }}

    [generators]
    AutotoolsToolchain
    AutotoolsDeps
    ```

    <br>

    Then, building your project is as easy as:

    ```shell
    $ conan install .
    # set the environment variables for Autotools
    $ source conanautotoolstoolchain.sh
    $ source conanautotoolsdeps.sh
    $ ./configure
    $ make

    # restore the environment after the build is completed
    $ source deactivate_conanautotoolstoolchain.sh
    $ source deactivate_conanautotoolsdeps.sh
    ```

    <br>

    ### [pkg-config Conan generator](https://docs.conan.io/en/latest/reference/conanfile/tools/gnu/pkgconfigdeps.html)

    <br>

    * [PkgConfigDeps](https://docs.conan.io/en/latest/reference/conanfile/tools/gnu/pkgconfigdeps.html): generates the **{{ pkgconfig_variables.pkg_name }}.pc** file (and the ones corresponding to **{{ requirement.ref.name }}** dependencies) with information about the dependencies that can be later used by the **pkg-config** tool pkg-config to collect data about the libraries Conan installed.

    <br>

    You can use this generator instead of the **AutotoolsDeps** one:

    ```ini
    [requires]
    {{ requirement }}

    [generators]
    AutotoolsToolchain
    PkgConfigDeps
    ```

    <br>

    And then using **pkg-config** to set the environment variables you want, like:

    ```shell
    $ conan install .
    # set the environment variables for Autotools
    $ source conanautotoolstoolchain.sh

    $ export CPPFLAGS="$CPPFLAGS $(pkg-config --cflags {{ requirement.ref.name }})"
    $ export LIBS="$LIBS $(pkg-config --libs-only-l {{ requirement.ref.name }})"
    $ export LDFLAGS="$LDFLAGS $(pkg-config --libs-only-L --libs-only-other {{ requirement.ref.name }})"

    $ ./configure
    $ make

    # restore the environment after the build is completed
    $ source deactivate_conanautotoolstoolchain.sh
    ```

    {% if requirement.cpp_info.has_components %}

    <br>

    As the {{ requirement.ref.name }} Conan package defines components you can use them to link only that desired part of the library in your project.  To check all the available components for **{{ requirement.ref.name }}** Conan package, and the corresponding *.pc* files names, please check the dedicated section at the end of this document.

    {%- endif %}
""")

buildsystem_other_tpl = textwrap.dedent("""

    <br>

    ## Other build systems

    <br>

    Please, [check the Conan documentation](https://docs.conan.io/en/latest/reference/conanfile/tools.html) for other integrations besides the ones listed in this document.

""")

components_tpl = textwrap.dedent("""

    {% if requirement.cpp_info.has_components %}

    <br>

    ## Declared components for {{ requirement.ref.name }}

    <br>

    These are all the declared components for the **{{ requirement.ref.name }}** Conan package:

    {%- for component_name, component_cpp_info in requirement.cpp_info.components.items() %}
    {%- if component_name %}
    * Component **{{ component_name }}**:
    {{- render_component_cpp_info(cmake_variables.component_alias[component_name], pkgconfig_variables.component_alias[component_name], component_cpp_info)|indent(width=2) }}
    {%- endif %}
    {%- endfor %}
    {%- endif %}
""")

headers_tpl = textwrap.dedent("""

    <br>

    ## Exposed header files for {{ requirement.ref.name }}

    <br>

    ```cpp
    {%- for header in headers %}
    #include <{{ header }}>
    {%- endfor %}
    ```

    <br>

""")


class MarkdownGenerator(Generator):
    @staticmethod
    def _list_headers(requirement):
        for include_dir in requirement.cpp_info.includedirs:
            for root, _, files in os.walk(os.path.join(requirement.package_folder, include_dir)):
                for f in files:
                    yield os.path.relpath(os.path.join(root, f), os.path.join(requirement.package_folder, include_dir))

    @staticmethod
    def _list_requires(requirement):
        return [(dep.ref.name, dep) for dep in requirement.dependencies.host.values()]

    @property
    def filename(self):
        pass

    @property
    def content(self):
        dict_loader = DictLoader({
            'macros': macros,
            'package.md': requirement_tpl,
            'buildsystem_cmake': buildsystem_cmake_tpl,
            'buildsystem_vs': buildsystem_vs_tpl,
            'buildsystem_autotools': buildsystem_autotools_tpl,
            'buildsystem_other': buildsystem_other_tpl,
            'components': components_tpl,
            'headers': headers_tpl
        })
        env = Environment(loader=dict_loader)
        template = env.get_template('package.md')

        def read_pkg_file(filename):
            try:
                return open(filename, 'r').read()
            except IOError:
                return '# Error reading file content. Please report.'

        env.filters['read_pkg_file'] = read_pkg_file

        from conans import __version__ as conan_version
        ret = {}
        for requirement in self.conanfile.dependencies.host.values():
            cmake_deps = CMakeDeps(self.conanfile)
            cmake_deps_template = CMakeDepsFileTemplate(cmake_deps,
                                                        requirement,
                                                        self.conanfile,
                                                        generating_module=False)

            cmake_component_alias = {
                component_name: cmake_deps_template.get_component_alias(requirement, component_name)
                for component_name, _
                in requirement.cpp_info.components.items()
                if component_name
            }

            project_type = 'C'
            project_extension = '.c'
            if requirement.settings.get_safe('compiler.libcxx') or requirement.settings.get_safe('compiler.cppstd'):
                project_type = 'CXX'
                project_extension = '.cpp'

            cmake_variables = {
                'global_target_name': requirement.cpp_info.get_property('cmake_target_name') or "{0}::{0}".format(requirement.ref.name),
                'component_alias': cmake_component_alias,
                'file_name': cmake_deps.get_cmake_package_name(requirement)
            }

            pkgconfig_component_alias = {
                component_name: pkgconfig_get_component_name(requirement, component_name) or
                                pkgconfig_get_name_with_namespace(pkgconfig_get_package_name(requirement), component_name)
                for component_name, _
                in requirement.cpp_info.components.items()
                if component_name
            }
            pkgconfig_variables = {
                'pkg_name': pkgconfig_get_package_name(requirement),
                'component_alias': pkgconfig_component_alias
            }

            ret["{}.md".format(requirement.ref.name)] = template.render(
                requirement=requirement,
                headers=self._list_headers(requirement),
                requires=list(self._list_requires(requirement)),
                cmake_variables=cmake_variables,
                pkgconfig_variables=pkgconfig_variables,
                package_folder=requirement.package_folder,
                relpath=os.path.relpath,
                conan_version=conan_version,
                now=datetime.datetime.now(),
                project_type=project_type,
                project_extension=project_extension
            )

        return ret