Skip to content

Commit

Permalink
[Bazel][1.x][Part 1]: Bazel + BazelToolchain refactor (#14958)
Browse files Browse the repository at this point in the history
* BazelDeps refactor

* BazelToolchain refactor

* bazel_layout refactor

* Bazel helper refactor

* Reverted BazelDeps

* Applying suggestions

* Applying suggestions

* removed Conan 2 imports

* Testing BazelToolchain and bazel_layout

* Fixed tests on macos

* Skipping windows

* reordering imports

* Building all the targets by default

* 1.x conftest tools

* test refactored

* Keeping baward compatibility. Safer command runner. Fixed Windows rc paths

* Better comment

* Removed configure from templates. Useless

* removed strip attr and fastbuild as default value

* removed strip attr

* Added validate step

* Validate test

* Keeping backward-compatibility. Added deprecated warnings

* Keeping original bazel_layout. Applied suggestions
  • Loading branch information
franramirez688 authored Oct 30, 2023
1 parent e992ec6 commit 49bc8e5
Show file tree
Hide file tree
Showing 11 changed files with 490 additions and 244 deletions.
110 changes: 75 additions & 35 deletions conan/tools/google/bazel.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,84 @@
from conan.tools.files.files import load_toolchain_args
import os
import platform

from conan.tools.google import BazelToolchain


class Bazel(object):

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

def configure(self, args=None):
# TODO: Remove in Conan 2.x. Keeping it backward compatible
self._conanfile.output.warning("Bazel.configure() function has been deprecated."
" Removing in Conan 2.x.")
pass

def build(self, args=None, label=None):
# TODO: Change the directory where bazel builds the project (by default, /var/tmp/_bazel_<username> )

bazelrc_path = '--bazelrc={}'.format(self._bazelrc_path) if self._bazelrc_path else ''
bazel_config = " ".join(['--config={}'.format(conf) for conf in self._bazel_config])

# arch = self._conanfile.settings.get_safe("arch")
# cpu = {
# "armv8": "arm64",
# "x86_64": ""
# }.get(arch, arch)
#
# command = 'bazel {} build --sandbox_debug --subcommands=pretty_print --cpu={} {} {}'.format(
# bazelrc_path,
# cpu,
# bazel_config,
# label
# )
command = 'bazel {} build {} {}'.format(
bazelrc_path,
bazel_config,
label
)

self._conanfile.run(command)

def _get_bazel_project_configuration(self):
toolchain_file_content = load_toolchain_args(self._conanfile.generators_folder,
namespace=self._namespace)
configs = toolchain_file_content.get("bazel_configs")
self._bazel_config = configs.split(",") if configs else []
self._bazelrc_path = toolchain_file_content.get("bazelrc_path")
def _safe_run_command(self, command):
"""
Windows is having problems for stopping bazel processes, so it ends up locking
some files if something goes wrong. Better to shut down the Bazel server after running
each command.
"""
try:
self._conanfile.run(command)
finally:
if platform.system() == "Windows":
self._conanfile.run("bazel shutdown")

def build(self, args=None, label=None, target="//..."):
"""
Runs "bazel <rcpaths> build <configs> <args> <targets>"
:param label: DEPRECATED: It'll disappear in Conan 2.x. It is the target label
:param target: It is the target label
:param args: list of extra arguments
:return:
"""
# TODO: Remove in Conan 2.x. Superseded by target
if label:
self._conanfile.output.warning("In Bazel.build() call, label param has been deprecated."
" Migrating to target.")
target = label
# Use BazelToolchain generated file if exists
conan_bazelrc = os.path.join(self._conanfile.generators_folder, BazelToolchain.bazelrc_name)
use_conan_config = os.path.exists(conan_bazelrc)
bazelrc_paths = []
bazelrc_configs = []
if use_conan_config:
bazelrc_paths.append(conan_bazelrc)
bazelrc_configs.append(BazelToolchain.bazelrc_config)
# User bazelrc paths have more prio than Conan one
# See more info in https://bazel.build/run/bazelrc
# TODO: Legacy Bazel allowed only one value. Remove for Conan 2.x and check list-type.
rc_paths = self._conanfile.conf.get("tools.google.bazel:bazelrc_path", default=[])
rc_paths = [rc_paths.strip()] if isinstance(rc_paths, str) else rc_paths
bazelrc_paths.extend(rc_paths)
command = "bazel"
for rc in bazelrc_paths:
command += f" --bazelrc={rc}"
command += " build"
# TODO: Legacy Bazel allowed only one value or several ones separate by commas.
# Remove for Conan 2.x and check list-type.
configs = self._conanfile.conf.get("tools.google.bazel:configs", default=[])
configs = [c.strip() for c in configs.split(",")] if isinstance(configs, str) else configs
bazelrc_configs.extend(configs)
for config in bazelrc_configs:
command += f" --config={config}"
if args:
command += " ".join(f" {arg}" for arg in args)
command += f" {target}"
self._safe_run_command(command)

def test(self, target=None):
"""
Runs "bazel test <target>"
"""
if self._conanfile.conf.get("tools.build:skip_test", check_type=bool) or target is None:
return
self._safe_run_command(f'bazel test {target}')
23 changes: 14 additions & 9 deletions conan/tools/google/layout.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import os


def bazel_layout(conanfile, generator=None, src_folder="."):
"""The layout for bazel is very limited, it builds in the root folder even specifying
"bazel --output_base=xxx" in the other hand I don't know how to inject a custom path so
the build can load the dependencies.bzl from the BazelDeps"""
conanfile.folders.build = ""
conanfile.folders.generators = ""
# used in test package for example, to know where the binaries are (editables not supported yet)
conanfile.cpp.build.bindirs = ["bazel-bin"]
conanfile.cpp.build.libdirs = ["bazel-bin"]
def bazel_layout(conanfile, src_folder="."):
"""Bazel layout is so limited. It does not allow to create its special symlinks in other
folder. See more information in https://bazel.build/remote/output-directories"""
subproject = conanfile.folders.subproject
conanfile.folders.source = src_folder if not subproject else os.path.join(subproject, src_folder)
conanfile.folders.build = "." # Bazel always build the whole project in the root folder
# FIXME: Keeping backward-compatibility. Defaulting to "conan" in Conan 2.x.
conanfile.output.warning("In bazel_layout() call, generators folder changes its default value "
"from './' to './conan/' in Conan 2.x")
conanfile.folders.generators = "."
# FIXME: used in test package for example, to know where the binaries are (editables not supported yet)?
conanfile.cpp.build.bindirs = [os.path.join(conanfile.folders.build, "bazel-bin")]
conanfile.cpp.build.libdirs = [os.path.join(conanfile.folders.build, "bazel-bin")]
164 changes: 149 additions & 15 deletions conan/tools/google/toolchain.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,158 @@
import textwrap

from jinja2 import Template

from conan.tools._check_build_profile import check_using_build_profile
from conan.tools.files.files import save_toolchain_args
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}"


class BazelToolchain(object):
# 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
self._namespace = namespace
# 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)

# Flags
# TODO: Should we read the buildenv to get flags?
self.extra_cxxflags = []
self.extra_cflags = []
self.extra_ldflags = []
self.extra_defines = []

# 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.extra_cxxflags + 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.extra_cflags + 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.extra_ldflags + 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.conlyopt + self.cflags)),
"cxxopt": " ".join(f"--cxxopt={flag}" for flag in (self.cxxopt + self.cxxflags)),
"linkopt": " ".join(f"--linkopt={flag}" for flag in (self.linkopt + 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):
content = {}
configs = ",".join(self._conanfile.conf.get("tools.google.bazel:configs",
default=[],
check_type=list))
if configs:
content["bazel_configs"] = configs

bazelrc = self._conanfile.conf.get("tools.google.bazel:bazelrc_path")
if bazelrc:
content["bazelrc_path"] = bazelrc

save_toolchain_args(content, namespace=self._namespace)
# check_duplicated_generator(self, self._conanfile) # uncomment for Conan 2.x
save(self._conanfile, BazelToolchain.bazelrc_name, self._content)
24 changes: 17 additions & 7 deletions conans/assets/templates/new_v2_bazel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
conanfile_sources_v2 = """
import os
from conan import ConanFile
from conan.errors import ConanException
from conan.tools.google import Bazel, bazel_layout
from conan.tools.files import copy
Expand All @@ -23,13 +24,20 @@ def config_options(self):
if self.settings.os == "Windows":
del self.options.fPIC
def validate(self):
if self.settings.os in ("Windows", "Macos") and self.options.shared:
raise ConanException("Windows and Macos needs extra BUILD configuration to be able "
"to create a shared library. Please, check this reference to "
"know more about it: https://bazel.build/reference/be/c-cpp")
def layout(self):
bazel_layout(self)
# DEPRECATED: Default generators folder will be "conan" in Conan 2.x
self.folders.generators = "conan"
def build(self):
bazel = Bazel(self)
bazel.configure()
bazel.build(label="//main:{name}")
bazel.build()
def package(self):
dest_lib = os.path.join(self.package_folder, "lib")
Expand Down Expand Up @@ -66,11 +74,12 @@ def requirements(self):
def build(self):
bazel = Bazel(self)
bazel.configure()
bazel.build(label="//main:example")
bazel.build()
def layout(self):
bazel_layout(self)
# DEPRECATED: Default generators folder will be "conan" in Conan 2.x
self.folders.generators = "conan"
def test(self):
if not cross_building(self):
Expand Down Expand Up @@ -104,7 +113,7 @@ def test(self):

_bazel_workspace = ""
_test_bazel_workspace = """
load("@//:dependencies.bzl", "load_conan_dependencies")
load("@//conan:dependencies.bzl", "load_conan_dependencies")
load_conan_dependencies()
"""

Expand All @@ -130,11 +139,12 @@ class {package_name}Conan(ConanFile):
def layout(self):
bazel_layout(self)
# DEPRECATED: Default generators folder will be "conan" in Conan 2.x
self.folders.generators = "conan"
def build(self):
bazel = Bazel(self)
bazel.configure()
bazel.build(label="//main:{name}")
bazel.build()
def package(self):
dest_bin = os.path.join(self.package_folder, "bin")
Expand Down
Loading

0 comments on commit 49bc8e5

Please sign in to comment.