Code View

mstring / mstring-1.3.1.2 / conanfile.py
import os

from conan import ConanFile
from conan.tools.cmake import CMake, CMakeDeps, CMakeToolchain, cmake_layout
from conan.tools.files import copy


def cmake_project_version(recipe_folder):
    """The single source of truth for this repo's version: the number on the
    ``project(... VERSION x.y.z[.w] ...)`` line in CMakeLists.txt. Bump it
    there — and only there — and this package's version follows. Set
    ``CI_PROJECT_VERSION`` in the environment to override (for CI that stamps
    its own)."""
    import os
    import re
    text = open(os.path.join(recipe_folder, "CMakeLists.txt"),
                encoding="utf-8").read()
    m = re.search(r"\bproject\s*\((?:[^)]*?\s)?VERSION\s+"
                  r"([0-9]+(?:\.[0-9]+){0,3})\b",
                  text, re.IGNORECASE | re.DOTALL)
    if not m:
        raise Exception("conanfile: no literal 'project(... VERSION ...)' "
                        "found in CMakeLists.txt")
    return os.getenv("CI_PROJECT_VERSION") or m.group(1)


class MStringRecipe(ConanFile):
    name = "mstring"
    # A code generator: consumers take it as a tool_requires and run the
    # `mstring` executable (or the mstring_generate() CMake function) at
    # build time. The generated code itself needs nothing but the standard
    # library.
    package_type = "application"

    license = "MIT"
    author = "Fehmi Demiralp <demiralp@fedem.eu>"
    description = ("mstring - generates C++ message functions and classes "
                   "(string, streamable, error, exception, syslog) from "
                   "localized message definitions")
    topics = ("i18n", "l10n", "localization", "messages", "code-generator", "cpp")


    settings = "os", "compiler", "build_type", "arch"
    # with_tests: build tests/ and examples/ and run them (ctest) in
    # `conan build` / `conan create`. They compile the code mstring
    # generates, so they are the real check of a change to the generator.
    #
    # with_docs: also build the CMake `docs` target (Doxygen → build/<cfg>/
    # docs/html/) during `conan build`. Off by default. Requires system
    # Doxygen (and Graphviz `dot` for the graphs).
    options = {"with_tests": [True, False], "with_docs": [True, False]}
    default_options = {"with_tests": True, "with_docs": False}

    exports_sources = (
        "CMakeLists.txt",
        "LICENSE",
        "README.md",
        "CHANGELOG.md",
        "mstring.pc.in",
        "cmake/*",
        "docs/*",
        "src/*",
        "tests/*",
        "examples/*",
    )

    def set_version(self):
        self.version = self.version or cmake_project_version(self.recipe_folder)

    def requirements(self):
        # The parser core. Linked into the executable only; nothing of it is
        # visible to a consumer of the generated code.
        self.requires("cparse/[>=1.2 <2]")

    def layout(self):
        cmake_layout(self)

    def generate(self):
        tc = CMakeToolchain(self)
        tc.cache_variables["TESTING"] = bool(self.options.with_tests)
        tc.cache_variables["EXAMPLE"] = bool(self.options.with_tests)
        tc.generate()
        CMakeDeps(self).generate()

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()
        # enable_testing() only runs when TESTING/EXAMPLE is on, and only then
        # is CTestTestfile.cmake generated: its presence says a test target exists.
        ctest_file = os.path.join(self.build_folder, "CTestTestfile.cmake")
        if self.options.with_tests and os.path.exists(ctest_file) and not self.conf.get(
                "tools.build:skip_test", default=False):
            cmake.test()
        if self.options.with_docs:
            cmake.build(target="docs")
            html = os.path.join(self.build_folder, "docs", "html", "index.html")
            self.output.success("Doxygen HTML tree: " + html)

    def package(self):
        CMake(self).install()
        # mstring runs as a build step — from `conan build`, but just as well
        # from a plain `cmake --build` or an IDE, where Conan's build
        # environment (LD_LIBRARY_PATH) is not active. Ship libcparse next to
        # it; the executable's RUNPATH is $ORIGIN/../lib.
        cparse = self.dependencies["cparse"].cpp_info
        for libdir in cparse.libdirs:
            copy(self, "libcparse.so*", libdir,
                 os.path.join(self.package_folder, "lib"), keep_path=False)
        copy(self, "LICENSE", self.source_folder,
             os.path.join(self.package_folder, "licenses"))

    def package_id(self):
        # Tests and docs do not change the executable.
        del self.info.options.with_tests
        del self.info.options.with_docs

    def package_info(self):
        self.cpp_info.includedirs = []
        self.cpp_info.libdirs = []
        self.cpp_info.bindirs = ["bin"]

        # find_package(mstring) — from a consumer whose CMakeDeps has
        # build_context_activated / build_context_build_modules = ["mstring"]
        # — brings in mstring_generate().
        module = os.path.join("lib", "cmake", "mstring", "mstring-generate.cmake")
        self.cpp_info.builddirs = [os.path.join("lib", "cmake", "mstring")]
        self.cpp_info.set_property("cmake_file_name", "mstring")
        self.cpp_info.set_property("cmake_build_modules", [module])
        self.cpp_info.set_property("pkg_config_name", "mstring")