conan-io/conan

View on GitHub
conan/tools/meson/toolchain.py

Summary

Maintainability
A
1 hr
Test Coverage
import os
import textwrap

from jinja2 import Template

from conan.tools._check_build_profile import check_using_build_profile
from conan.tools._compilers import libcxx_flags
from conan.tools.apple.apple import to_apple_arch, is_apple_os, apple_min_version_flag, apple_sdk_path
from conan.tools.build.cross_building import cross_building, get_cross_building_settings
from conan.tools.env import VirtualBuildEnv
from conan.tools.meson.helpers import *
from conan.tools.microsoft import VCVars, msvc_runtime_flag
from conans.errors import ConanException
from conans.util.files import save


class MesonToolchain(object):
    native_filename = "conan_meson_native.ini"
    cross_filename = "conan_meson_cross.ini"

    _meson_file_template = textwrap.dedent("""
    [properties]
    {% for it, value in properties.items() -%}
    {{it}} = {{value}}
    {% endfor %}

    [constants]
    preprocessor_definitions = [{% for it, value in preprocessor_definitions.items() -%}
    '-D{{ it }}="{{ value}}"'{%- if not loop.last %}, {% endif %}{% endfor %}]
    # Constants to be overridden by conan_meson_deps_flags.ini (if exists)
    deps_c_args = []
    deps_c_link_args = []
    deps_cpp_args = []
    deps_cpp_link_args = []

    [project options]
    {% for it, value in project_options.items() -%}
    {{it}} = {{value}}
    {% endfor %}

    [binaries]
    {% if c %}c = '{{c}}'{% endif %}
    {% if cpp %}cpp = '{{cpp}}'{% endif %}
    {% if is_apple_system %}
    {% if objc %}objc = '{{objc}}'{% endif %}
    {% if objcpp %}objcpp = '{{objcpp}}'{% endif %}
    {% endif %}
    {% if c_ld %}c_ld = '{{c_ld}}'{% endif %}
    {% if cpp_ld %}cpp_ld = '{{cpp_ld}}'{% endif %}
    {% if ar %}ar = '{{ar}}'{% endif %}
    {% if strip %}strip = '{{strip}}'{% endif %}
    {% if as %}as = '{{as}}'{% endif %}
    {% if windres %}windres = '{{windres}}'{% endif %}
    {% if pkgconfig %}pkgconfig = '{{pkgconfig}}'{% endif %}

    [built-in options]
    {% if buildtype %}buildtype = '{{buildtype}}'{% endif %}
    {% if debug %}debug = {{debug}}{% endif %}
    {% if default_library %}default_library = '{{default_library}}'{% endif %}
    {% if b_vscrt %}b_vscrt = '{{b_vscrt}}' {% endif %}
    {% if b_ndebug %}b_ndebug = {{b_ndebug}}{% endif %}
    {% if b_staticpic %}b_staticpic = {{b_staticpic}}{% endif %}
    {% if cpp_std %}cpp_std = '{{cpp_std}}' {% endif %}
    {% if backend %}backend = '{{backend}}' {% endif %}
    {% if pkg_config_path %}pkg_config_path = '{{pkg_config_path}}'{% endif %}
    # C/C++ arguments
    c_args = {{c_args}} + preprocessor_definitions + deps_c_args
    c_link_args = {{c_link_args}} + deps_c_link_args
    cpp_args = {{cpp_args}} + preprocessor_definitions + deps_cpp_args
    cpp_link_args = {{cpp_link_args}} + deps_cpp_link_args
    {% if is_apple_system %}
    # Objective-C/C++ arguments
    objc_args = {{objc_args}} + preprocessor_definitions + deps_c_args
    objc_link_args = {{objc_link_args}} + deps_c_link_args
    objcpp_args = {{objcpp_args}} + preprocessor_definitions + deps_cpp_args
    objcpp_link_args = {{objcpp_link_args}} + deps_cpp_link_args
    {% endif %}

    {% for context, values in cross_build.items() %}
    [{{context}}_machine]
    system = '{{values["system"]}}'
    cpu_family = '{{values["cpu_family"]}}'
    cpu = '{{values["cpu"]}}'
    endian = '{{values["endian"]}}'
    {% endfor %}
    """)

    def __init__(self, conanfile, backend=None):
        self._conanfile = conanfile
        self._os = self._conanfile.settings.get_safe("os")
        self._is_apple_system = is_apple_os(self._conanfile)

        # Values are kept as Python built-ins so users can modify them more easily, and they are
        # only converted to Meson file syntax for rendering
        # priority: first user conf, then recipe, last one is default "ninja"
        self._backend = conanfile.conf.get("tools.meson.mesontoolchain:backend",
                                           default=backend or 'ninja')
        build_type = self._conanfile.settings.get_safe("build_type")
        self._buildtype = {"Debug": "debug",  # Note, it is not "'debug'"
                           "Release": "release",
                           "MinSizeRel": "minsize",
                           "RelWithDebInfo": "debugoptimized"}.get(build_type, build_type)
        self._b_ndebug = "true" if self._buildtype != "debug" else "false"

        # https://mesonbuild.com/Builtin-options.html#base-options
        fpic = self._conanfile.options.get_safe("fPIC")
        shared = self._conanfile.options.get_safe("shared")
        self._b_staticpic = fpic if (shared is False and fpic is not None) else None
        # https://mesonbuild.com/Builtin-options.html#core-options
        # Do not adjust "debug" if already adjusted "buildtype"
        self._default_library = ("shared" if shared else "static") if shared is not None else None

        compiler = self._conanfile.settings.get_safe("compiler")
        if compiler is None:
            raise ConanException("MesonToolchain needs 'settings.compiler', but it is not defined")
        cppstd = self._conanfile.settings.get_safe("compiler.cppstd")
        self._cpp_std = to_cppstd_flag(compiler, cppstd)

        if compiler == "Visual Studio":
            vscrt = self._conanfile.settings.get_safe("compiler.runtime")
            self._b_vscrt = str(vscrt).lower()
        elif compiler == "msvc":
            vscrt = msvc_runtime_flag(self._conanfile)
            self._b_vscrt = str(vscrt).lower()
        else:
            self._b_vscrt = None

        self.properties = {}
        self.project_options = {
            "wrap_mode": "nofallback"  # https://github.com/conan-io/conan/issues/10671
        }
        # Add all the default dirs
        self.project_options.update(self._get_default_dirs())

        self.preprocessor_definitions = {}
        self.pkg_config_path = self._conanfile.generators_folder
        self.libcxx, self.gcc_cxx11_abi = libcxx_flags(self._conanfile)

        check_using_build_profile(self._conanfile)

        self.cross_build = {}
        default_comp = ""
        default_comp_cpp = ""
        if cross_building(conanfile, skip_x64_x86=True):
            os_build, arch_build, os_host, arch_host = get_cross_building_settings(self._conanfile)
            self.cross_build["build"] = to_meson_machine(os_build, arch_build)
            self.cross_build["host"] = to_meson_machine(os_host, arch_host)
            self.properties["needs_exe_wrapper"] = True
            if hasattr(conanfile, 'settings_target') and conanfile.settings_target:
                settings_target = conanfile.settings_target
                os_target = settings_target.get_safe("os")
                arch_target = settings_target.get_safe("arch")
                self.cross_build["target"] = to_meson_machine(os_target, arch_target)
            if is_apple_os(self._conanfile):  # default cross-compiler in Apple is common
                default_comp = "clang"
                default_comp_cpp = "clang++"
        else:
            if "clang" in compiler:
                default_comp = "clang"
                default_comp_cpp = "clang++"
            elif compiler == "gcc":
                default_comp = "gcc"
                default_comp_cpp = "g++"
        if "Visual" in compiler or compiler == "msvc":
            default_comp = "cl"
            default_comp_cpp = "cl"

        # Read configuration for compilers
        compilers_by_conf = self._conanfile.conf.get("tools.build:compiler_executables", default={},
                                                     check_type=dict)
        # Read the VirtualBuildEnv to update the variables
        build_env = VirtualBuildEnv(self._conanfile).vars()
        self.c = compilers_by_conf.get("c") or build_env.get("CC") or default_comp
        self.cpp = compilers_by_conf.get("cpp") or build_env.get("CXX") or default_comp_cpp
        # FIXME: Should we use the new tools.build:compiler_executables and avoid buildenv?
        self.c_ld = build_env.get("CC_LD") or build_env.get("LD")
        self.cpp_ld = build_env.get("CXX_LD") or build_env.get("LD")
        self.ar = build_env.get("AR")
        self.strip = build_env.get("STRIP")
        self.as_ = build_env.get("AS")
        self.windres = build_env.get("WINDRES")
        self.pkgconfig = build_env.get("PKG_CONFIG")
        self.c_args = self._get_env_list(build_env.get("CFLAGS", []))
        self.c_link_args = self._get_env_list(build_env.get("LDFLAGS", []))
        self.cpp_args = self._get_env_list(build_env.get("CXXFLAGS", []))
        self.cpp_link_args = self._get_env_list(build_env.get("LDFLAGS", []))

        # Apple flags and variables
        self.apple_arch_flag = []
        self.apple_isysroot_flag = []
        self.apple_min_version_flag = []
        self.objc = None
        self.objcpp = None
        self.objc_args = []
        self.objc_link_args = []
        self.objcpp_args = []
        self.objcpp_link_args = []

        self._resolve_apple_flags_and_variables(build_env, compilers_by_conf)
        self._resolve_android_cross_compilation()

    def _get_default_dirs(self):
        """
        Get all the default directories from cpp.package.

        Issues related:
            - https://github.com/conan-io/conan/issues/9713
            - https://github.com/conan-io/conan/issues/11596
        """
        def _get_cpp_info_value(name):
            elements = getattr(self._conanfile.cpp.package, name)
            return elements[0] if elements else None

        ret = {}
        bindir = _get_cpp_info_value("bindirs")
        datadir = _get_cpp_info_value("resdirs")
        libdir = _get_cpp_info_value("libdirs")
        includedir = _get_cpp_info_value("includedirs")
        if bindir:
            ret.update({
                'bindir': bindir,
                'sbindir': bindir,
                'libexecdir': bindir
            })
        if datadir:
            ret.update({
                'datadir': datadir,
                'localedir': datadir,
                'mandir': datadir,
                'infodir': datadir
            })
        if includedir:
            ret["includedir"] = includedir
        if libdir:
            ret["libdir"] = libdir
        return ret

    def _resolve_apple_flags_and_variables(self, build_env, compilers_by_conf):
        if not self._is_apple_system:
            return
        # SDK path is mandatory for cross-building
        sdk_path = apple_sdk_path(self._conanfile)
        if not sdk_path and self.cross_build:
            raise ConanException(
                "Apple SDK path not found. For cross-compilation, you must "
                "provide a valid SDK path in 'tools.apple:sdk_path' config."
            )

        # TODO: Delete this os_sdk check whenever the _guess_apple_sdk_name() function disappears
        os_sdk = self._conanfile.settings.get_safe('os.sdk')
        if not os_sdk and self._os != "Macos":
            raise ConanException("Please, specify a suitable value for os.sdk.")

        # Calculating the main Apple flags
        arch = to_apple_arch(self._conanfile)
        self.apple_arch_flag = ["-arch", arch] if arch else []
        self.apple_isysroot_flag = ["-isysroot", sdk_path] if sdk_path else []
        self.apple_min_version_flag = [apple_min_version_flag(self._conanfile)]
        # Objective C/C++ ones
        self.objc = compilers_by_conf.get("objc", "clang")
        self.objcpp = compilers_by_conf.get("objcpp", "clang++")
        self.objc_args = self._get_env_list(build_env.get('OBJCFLAGS', []))
        self.objc_link_args = self._get_env_list(build_env.get('LDFLAGS', []))
        self.objcpp_args = self._get_env_list(build_env.get('OBJCXXFLAGS', []))
        self.objcpp_link_args = self._get_env_list(build_env.get('LDFLAGS', []))

    def _resolve_android_cross_compilation(self):
        if not self.cross_build or not self.cross_build["host"]["system"] == "android":
            return

        ndk_path = self._conanfile.conf.get("tools.android:ndk_path")
        if not ndk_path:
            raise ConanException("You must provide a NDK path. Use 'tools.android:ndk_path' "
                                 "configuration field.")

        arch = self._conanfile.settings.get_safe("arch")
        os_build = self.cross_build["build"]["system"]
        ndk_bin = os.path.join(ndk_path, "toolchains", "llvm", "prebuilt", "{}-x86_64".format(os_build), "bin")
        android_api_level = self._conanfile.settings.get_safe("os.api_level")
        android_target = {'armv7': 'armv7a-linux-androideabi',
                          'armv8': 'aarch64-linux-android',
                          'x86': 'i686-linux-android',
                          'x86_64': 'x86_64-linux-android'}.get(arch)
        self.c = os.path.join(ndk_bin, "{}{}-clang".format(android_target, android_api_level))
        self.cpp = os.path.join(ndk_bin, "{}{}-clang++".format(android_target, android_api_level))
        self.ar = os.path.join(ndk_bin, "llvm-ar")

    def _get_extra_flags(self):
        # Now, it's time to get all the flags defined by the user
        cxxflags = self._conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list)
        cflags = self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list)
        sharedlinkflags = self._conanfile.conf.get("tools.build:sharedlinkflags", default=[], check_type=list)
        exelinkflags = self._conanfile.conf.get("tools.build:exelinkflags", default=[], check_type=list)
        linker_scripts = self._conanfile.conf.get("tools.build:linker_scripts", default=[], check_type=list)
        linker_script_flags = ['-T"' + linker_script + '"' for linker_script in linker_scripts]
        return {
            "cxxflags": cxxflags,
            "cflags": cflags,
            "ldflags": sharedlinkflags + exelinkflags + linker_script_flags
        }

    @staticmethod
    def _get_env_list(v):
        # FIXME: Should Environment have the "check_type=None" keyword as Conf?
        return v.strip().split() if not isinstance(v, list) else v

    @staticmethod
    def _filter_list_empty_fields(v):
        return list(filter(bool, v))

    def _context(self):
        apple_flags = self.apple_isysroot_flag + self.apple_arch_flag + self.apple_min_version_flag
        extra_flags = self._get_extra_flags()

        self.c_args.extend(apple_flags + extra_flags["cflags"])
        self.cpp_args.extend(apple_flags + extra_flags["cxxflags"])
        self.c_link_args.extend(apple_flags + extra_flags["ldflags"])
        self.cpp_link_args.extend(apple_flags + extra_flags["ldflags"])
        # Objective C/C++
        self.objc_args.extend(self.c_args)
        self.objcpp_args.extend(self.cpp_args)
        # These link_args have already the LDFLAGS env value so let's add only the new possible ones
        self.objc_link_args.extend(apple_flags + extra_flags["ldflags"])
        self.objcpp_link_args.extend(apple_flags + extra_flags["ldflags"])

        if self.libcxx:
            self.cpp_args.append(self.libcxx)
            self.cpp_link_args.append(self.libcxx)
        if self.gcc_cxx11_abi:
            self.cpp_args.append("-D{}".format(self.gcc_cxx11_abi))

        return {
            # https://mesonbuild.com/Machine-files.html#properties
            "properties": {k: to_meson_value(v) for k, v in self.properties.items()},
            # https://mesonbuild.com/Machine-files.html#project-specific-options
            "project_options": {k: to_meson_value(v) for k, v in self.project_options.items()},
            # https://mesonbuild.com/Builtin-options.html#directories
            # TODO : we don't manage paths like libdir here (yet?)
            # https://mesonbuild.com/Machine-files.html#binaries
            # https://mesonbuild.com/Reference-tables.html#compiler-and-linker-selection-variables
            "c": self.c,
            "cpp": self.cpp,
            "objc": self.objc,
            "objcpp": self.objcpp,
            "c_ld": self.c_ld,
            "cpp_ld": self.cpp_ld,
            "ar": self.ar,
            "strip": self.strip,
            "as": self.as_,
            "windres": self.windres,
            "pkgconfig": self.pkgconfig,
            # https://mesonbuild.com/Builtin-options.html#core-options
            "buildtype": self._buildtype,
            "default_library": self._default_library,
            "backend": self._backend,
            # https://mesonbuild.com/Builtin-options.html#base-options
            "b_vscrt": self._b_vscrt,
            "b_staticpic": to_meson_value(self._b_staticpic),  # boolean
            "b_ndebug": to_meson_value(self._b_ndebug),  # boolean as string
            # https://mesonbuild.com/Builtin-options.html#compiler-options
            "cpp_std": self._cpp_std,
            "c_args": to_meson_value(self._filter_list_empty_fields(self.c_args)),
            "c_link_args": to_meson_value(self._filter_list_empty_fields(self.c_link_args)),
            "cpp_args": to_meson_value(self._filter_list_empty_fields(self.cpp_args)),
            "cpp_link_args": to_meson_value(self._filter_list_empty_fields(self.cpp_link_args)),
            "objc_args": to_meson_value(self._filter_list_empty_fields(self.objc_args)),
            "objc_link_args": to_meson_value(self._filter_list_empty_fields(self.objc_link_args)),
            "objcpp_args": to_meson_value(self._filter_list_empty_fields(self.objcpp_args)),
            "objcpp_link_args": to_meson_value(self._filter_list_empty_fields(self.objcpp_link_args)),
            "pkg_config_path": self.pkg_config_path,
            "preprocessor_definitions": self.preprocessor_definitions,
            "cross_build": self.cross_build,
            "is_apple_system": self._is_apple_system
        }

    @property
    def content(self):
        context = self._context()
        content = Template(self._meson_file_template).render(context)
        return content

    def generate(self):
        filename = self.native_filename if not self.cross_build else self.cross_filename
        save(filename, self.content)
        # FIXME: Should we check the OS and compiler to call VCVars?
        VCVars(self._conanfile).generate()