conan-io/conan

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

Summary

Maintainability
A
0 mins
Test Coverage
import textwrap

from jinja2 import Template

from conan.tools._check_build_profile import check_using_build_profile
from conan.tools._compilers import cppstd_flag
from conan.tools.apple import to_apple_arch, is_apple_os
from conan.tools.build.cross_building import cross_building
from conan.tools.files import save


def _get_cpu_name(conanfile):
    host_os = conanfile.settings.get_safe('os').lower()
    host_arch = conanfile.settings.get_safe('arch')
    if is_apple_os(conanfile):
        host_os = "darwin" if host_os == "macos" else host_os
        host_arch = to_apple_arch(conanfile)
    # FIXME: Probably it's going to fail, but let's try it because it normally follows this syntax
    return f"{host_os}_{host_arch}"


# FIXME: In the future, it could be BazelPlatform instead? Check https://bazel.build/concepts/platforms
class BazelToolchain:
    """
    Creates a simple conan_bzl.rc file which defines a conan-config configuration with all the
    attributes defined by the consumer. Bear in mind that this is not a complete toolchain, it
    only fills some common CLI attributes and save them in a *.rc file.

    Important: Maybe, this toolchain should create a new Conan platform with the user
    constraints, but it's not the goal for now as Bazel has tons of platforms and toolchains
    already available in its bazel_tools repo. For now, it only admits a list of platforms defined
    by the user.
    More information related:
        * Toolchains: https://bazel.build/extending/toolchains (deprecated)
        * Platforms: https://bazel.build/concepts/platforms (new default since Bazel 7.x)
        * Migrating to platforms: https://bazel.build/concepts/platforms
        * Issue related: https://github.com/bazelbuild/bazel/issues/6516

    Others:
        CROOSTOOL: https://github.com/bazelbuild/bazel/blob/cb0fb033bad2a73e0457f206afb87e195be93df2/tools/cpp/CROSSTOOL
        Cross-compiling with Bazel: https://ltekieli.com/cross-compiling-with-bazel/
        bazelrc files: https://bazel.build/run/bazelrc
        CLI options: https://bazel.build/reference/command-line-reference
        User manual: https://bazel.build/docs/user-manual
    """

    bazelrc_name = "conan_bzl.rc"
    bazelrc_config = "conan-config"
    bazelrc_template = textwrap.dedent("""\
    # Automatic bazelrc file created by Conan
    {% if copt %}build:conan-config {{copt}}{% endif %}
    {% if conlyopt %}build:conan-config {{conlyopt}}{% endif %}
    {% if cxxopt %}build:conan-config {{cxxopt}}{% endif %}
    {% if linkopt %}build:conan-config {{linkopt}}{% endif %}
    {% if force_pic %}build:conan-config --force_pic={{force_pic}}{% endif %}
    {% if dynamic_mode %}build:conan-config --dynamic_mode={{dynamic_mode}}{% endif %}
    {% if compilation_mode %}build:conan-config --compilation_mode={{compilation_mode}}{% endif %}
    {% if compiler %}build:conan-config --compiler={{compiler}}{% endif %}
    {% if cpu %}build:conan-config --cpu={{cpu}}{% endif %}
    {% if crosstool_top %}build:conan-config --crosstool_top={{crosstool_top}}{% endif %}""")

    def __init__(self, conanfile, namespace=None):
        self._conanfile = conanfile
        # TODO: Remove namespace and check_using_build_profile in Conan 2.x
        if namespace:
            self._conanfile.output.warning("In BazelToolchain() call, namespace param has been "
                                        "deprecated as it's not used anymore.")
        check_using_build_profile(self._conanfile)

        # Bazel build parameters
        shared = self._conanfile.options.get_safe("shared")
        fpic = self._conanfile.options.get_safe("fPIC")
        self.force_pic = fpic if (not shared and fpic is not None) else None
        # FIXME: Keeping this option but it's not working as expected. It's not creating the shared
        #        libraries at all.
        self.dynamic_mode = "fully" if shared else "off"
        self.cppstd = cppstd_flag(self._conanfile.settings)
        self.copt = []
        self.conlyopt = []
        self.cxxopt = []
        self.linkopt = []
        self.compilation_mode = {'Release': 'opt', 'Debug': 'dbg'}.get(
            self._conanfile.settings.get_safe("build_type")
        )
        # Be aware that this parameter does not admit a compiler absolute path
        # If you want to add it, you will have to use a specific Bazel toolchain
        self.compiler = None
        # cpu is the target architecture, and it's a bit tricky. If it's not a cross-compilation,
        # let Bazel guess it.
        self.cpu = None
        # TODO: cross-compilation process is so powerless. Needs to use the new platforms.
        if cross_building(self._conanfile):
            # Bazel is using those toolchains/platforms by default.
            # It's better to let it configure the project in that case
            self.cpu = _get_cpu_name(conanfile)
        # This is itself a toolchain but just in case
        self.crosstool_top = None
        # TODO: Have a look at https://bazel.build/reference/be/make-variables
        # FIXME: Missing host_xxxx options. When are they needed? Cross-compilation?

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

    @property
    def cxxflags(self):
        ret = [self.cppstd]
        conf_flags = self._conanfile.conf.get("tools.build:cxxflags", default=[], check_type=list)
        ret = ret  + self.cxxopt + conf_flags
        return self._filter_list_empty_fields(ret)

    @property
    def cflags(self):
        conf_flags = self._conanfile.conf.get("tools.build:cflags", default=[], check_type=list)
        ret = self.conlyopt + conf_flags
        return self._filter_list_empty_fields(ret)

    @property
    def ldflags(self):
        conf_flags = self._conanfile.conf.get("tools.build:sharedlinkflags", default=[],
                                              check_type=list)
        conf_flags.extend(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)
        conf_flags.extend(["-T'" + linker_script + "'" for linker_script in linker_scripts])
        ret = self.linkopt + conf_flags
        return self._filter_list_empty_fields(ret)

    def _context(self):
        return {
            "copt": " ".join(f"--copt={flag}" for flag in self.copt),
            "conlyopt": " ".join(f"--conlyopt={flag}" for flag in self.cflags),
            "cxxopt": " ".join(f"--cxxopt={flag}" for flag in self.cxxflags),
            "linkopt": " ".join(f"--linkopt={flag}" for flag in self.ldflags),
            "force_pic": self.force_pic,
            "dynamic_mode": self.dynamic_mode,
            "compilation_mode": self.compilation_mode,
            "compiler": self.compiler,
            "cpu": self.cpu,
            "crosstool_top": self.crosstool_top,
        }

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

    def generate(self):
        # check_duplicated_generator(self, self._conanfile)  # uncomment for Conan 2.x
        save(self._conanfile, BazelToolchain.bazelrc_name, self._content)