diff --git a/conan/tools/google/__init__.py b/conan/tools/google/__init__.py new file mode 100644 index 00000000000..6888b003776 --- /dev/null +++ b/conan/tools/google/__init__.py @@ -0,0 +1,3 @@ +from conan.tools.google.toolchain import BazelToolchain +from conan.tools.google.bazeldeps import BazelDeps +from conan.tools.google.bazel import Bazel diff --git a/conan/tools/google/bazel.py b/conan/tools/google/bazel.py new file mode 100644 index 00000000000..a0282ca4bf3 --- /dev/null +++ b/conan/tools/google/bazel.py @@ -0,0 +1,49 @@ +import os +import json + +from conan.tools import CONAN_TOOLCHAIN_ARGS_FILE +from conans.util.files import load + +class Bazel(object): + def __init__(self, conanfile): + self._conanfile = conanfile + self._get_bazel_project_configuration() + + def configure(self, args=None): + pass + + def build(self, args=None, label=None): + # TODO: Change the directory where bazel builds the project (by default, /var/tmp/_bazel_ ) + + bazelrc_path = '--bazelrc={}'.format(self._bazelrc_path) if self._bazelrc_path else '' + bazel_config = '--config={}'.format(self._bazel_config) if self._bazel_config else '' + + # 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): + self._bazel_config = None + self._bazelrc_path = None + + if os.path.exists(CONAN_TOOLCHAIN_ARGS_FILE): + conan_toolchain_args = json.loads(load(CONAN_TOOLCHAIN_ARGS_FILE)) + self._bazel_config = conan_toolchain_args.get("bazel_config", None) + self._bazelrc_path = conan_toolchain_args.get("bazelrc_path", None) diff --git a/conan/tools/google/bazeldeps.py b/conan/tools/google/bazeldeps.py new file mode 100644 index 00000000000..f9376cf9df2 --- /dev/null +++ b/conan/tools/google/bazeldeps.py @@ -0,0 +1,122 @@ +import textwrap + +from jinja2 import Template + +from conans.util.files import save + + +class BazelDeps(object): + def __init__(self, conanfile): + self._conanfile = conanfile + + def generate(self): + local_repositories = [] + for dependency in self._conanfile.dependencies.transitive_host_requires: + content = self._get_dependency_buildfile_content(dependency) + filename = self._save_dependendy_buildfile(dependency, content) + + local_repository = self._create_new_local_repository(dependency, filename) + local_repositories.append(local_repository) + + content = self._get_main_buildfile_content(local_repositories) + self._save_main_buildfiles(content) + + def _save_dependendy_buildfile(self, dependency, buildfile_content): + filename = 'conandeps/{}/BUILD'.format(dependency.ref.name) + save(filename, buildfile_content) + return filename + + def _get_dependency_buildfile_content(self, dependency): + template = textwrap.dedent(""" + load("@rules_cc//cc:defs.bzl", "cc_import", "cc_library") + + {% for lib in libs %} + cc_import( + name = "{{ lib }}_precompiled", + static_library = "{{ libdir }}/lib{{ lib }}.a" + ) + {% endfor %} + + cc_library( + name = "{{ name }}", + {% if headers %} + hdrs = glob([{{ headers }}]), + {% endif %} + {% if includes %} + includes = [{{ includes }}], + {% endif %} + {% if defines %} + defines = [{{ defines }}], + {% endif %} + visibility = ["//visibility:public"] + ) + + """) + + dependency.new_cpp_info.aggregate_components() + + if not dependency.new_cpp_info.libs and not dependency.new_cpp_info.includedirs: + return None + + headers = [] + includes = [] + + for path in dependency.new_cpp_info.includedirs: + headers.append('"{}/**"'.format(path)) + includes.append('"{}"'.format(path)) + + headers = ', '.join(headers) + includes = ', '.join(includes) + + defines = ('"{}"'.format(define) for define in dependency.new_cpp_info.defines) + defines = ', '.join(defines) + + context = { + "name": dependency.ref.name, + "libs": dependency.new_cpp_info.libs, + "libdir": dependency.new_cpp_info.libdirs[0], + "headers": headers, + "includes": includes, + "defines": defines + } + + content = Template(template).render(**context) + return content + + def _create_new_local_repository(self, dependency, dependency_buildfile_name): + snippet = textwrap.dedent(""" + native.new_local_repository( + name="{}", + path="{}", + build_file="{}", + ) + """).format( + dependency.ref.name, + dependency.package_folder, + dependency_buildfile_name + ) + + return snippet + + def _get_main_buildfile_content(self, local_repositories): + template = textwrap.dedent(""" + def load_conan_dependencies(): + {} + """) + + if local_repositories: + function_content = "\n".join(local_repositories) + function_content = ' '.join(line for line in function_content.splitlines(True)) + else: + function_content = ' pass' + + content = template.format(function_content) + + return content + + def _save_main_buildfiles(self, content): + # A BUILD file must exist, even if it's empty, in order for bazel + # to detect it as a bazel package and allow to load the .bzl files + save("conandeps/BUILD", "") + + save("conandeps/dependencies.bzl", content) diff --git a/conan/tools/google/toolchain.py b/conan/tools/google/toolchain.py new file mode 100644 index 00000000000..cb518fd7cdb --- /dev/null +++ b/conan/tools/google/toolchain.py @@ -0,0 +1,19 @@ +import json + +from conan.tools import CONAN_TOOLCHAIN_ARGS_FILE +from conans.util.files import save + + +class BazelToolchain(object): + + def __init__(self, conanfile): + self._conanfile = conanfile + + def generate(self): + bazel_config = self._conanfile.conf["tools.google.bazel:config"] + bazelrc_path = self._conanfile.conf["tools.google.bazel:bazelrc_path"] + + save(CONAN_TOOLCHAIN_ARGS_FILE, json.dumps({ + "bazel_config": bazel_config, + "bazelrc_path": bazelrc_path + })) diff --git a/conans/client/generators/__init__.py b/conans/client/generators/__init__.py index d19cdaafb86..e5e41464aa3 100644 --- a/conans/client/generators/__init__.py +++ b/conans/client/generators/__init__.py @@ -67,7 +67,8 @@ def __init__(self): "markdown": MarkdownGenerator} self._new_generators = ["CMakeToolchain", "CMakeDeps", "MSBuildToolchain", "MesonToolchain", "MSBuildDeps", "QbsToolchain", "msbuild", - "VirtualEnv", "AutotoolsDeps", "AutotoolsToolchain", "AutotoolsGen"] + "VirtualEnv", "AutotoolsDeps", "AutotoolsToolchain", "AutotoolsGen", + "BazelDeps", "BazelToolchain"] def add(self, name, generator_class, custom=False): if name not in self._generators or custom: @@ -120,6 +121,12 @@ def _new_generator(self, generator_name, output): elif generator_name == "VirtualEnv": from conan.tools.env.virtualenv import VirtualEnv return VirtualEnv + elif generator_name == "BazelDeps": + from conan.tools.google import BazelDeps + return BazelDeps + elif generator_name == "BazelToolchain": + from conan.tools.google import BazelToolchain + return BazelToolchain else: raise ConanException("Internal Conan error: Generator '{}' " "not commplete".format(generator_name)) diff --git a/conans/test/conftest.py b/conans/test/conftest.py index 5db1194c109..739bd4efd1a 100644 --- a/conans/test/conftest.py +++ b/conans/test/conftest.py @@ -13,7 +13,8 @@ 'cygwin': 'default', 'mingw32': 'default', 'mingw64': 'default', - 'ninja': '1.10.2' + 'ninja': '1.10.2', + 'bazel': 'default' } tools_locations = { @@ -41,7 +42,11 @@ '3.19': '/usr/share/cmake-3.19.7/bin' } }, - 'ninja': {'Windows': {'1.10.2': 'C:/Tools/ninja/1.10.2'}} + 'ninja': {'Windows': {'1.10.2': 'C:/Tools/ninja/1.10.2'}}, + 'bazel': { + 'Darwin': {'default': '/Users/jenkins/bin'}, + 'Windows': {'default': 'C:/bazel/bin'}, + } } tools_environments = { @@ -54,6 +59,7 @@ 'gcc', 'clang', 'visual_studio', 'xcode', 'msys2', 'cygwin', 'mingw32', 'mingw64', 'autotools', 'pkg_config', 'premake', 'meson', 'ninja', + 'bazel', 'file', 'git', 'svn', 'compiler', diff --git a/conans/test/functional/toolchains/google/__init__.py b/conans/test/functional/toolchains/google/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/conans/test/functional/toolchains/google/test_bazel.py b/conans/test/functional/toolchains/google/test_bazel.py new file mode 100644 index 00000000000..d9ce31b2c8b --- /dev/null +++ b/conans/test/functional/toolchains/google/test_bazel.py @@ -0,0 +1,193 @@ +import os +import platform +import textwrap +import time +import unittest + +import pytest +from parameterized.parameterized import parameterized + +from conans.model.ref import ConanFileReference, PackageReference +from conans.test.assets.cmake import gen_cmakelists +from conans.test.assets.sources import gen_function_cpp, gen_function_h +from conans.test.functional.utils import check_vs_runtime, check_exe_run +from conans.test.utils.tools import TestClient +from conans.util.files import save + + +@pytest.mark.toolchain +@pytest.mark.tool_bazel +class Base(unittest.TestCase): + + conanfile = textwrap.dedent(""" + from conan.tools.google import Bazel + from conans import ConanFile + + class App(ConanFile): + name="test_bazel_app" + version="0.0" + settings = "os", "compiler", "build_type", "arch" + generators = "BazelDeps", "BazelToolchain" + exports_sources = "WORKSPACE", "app/*" + + def build(self): + bazel = Bazel(self) + bazel.configure() + bazel.build(label="//app:main") + + def package(self): + self.copy('*', src='bazel-bin') + """) + + buildfile = textwrap.dedent(""" + load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") + + cc_library( + name = "hello", + srcs = ["hello.cpp"], + hdrs = ["hello.h",], + ) + + cc_binary( + name = "main", + srcs = ["main.cpp"], + deps = [":hello"], + ) +""") + + lib_h = gen_function_h(name="hello") + lib_cpp = gen_function_cpp(name="hello", msg="Hello", includes=["hello"]) + main = gen_function_cpp(name="main", includes=["hello"], calls=["hello"]) + + workspace_file = textwrap.dedent(""" + load("@//bazel-build/conandeps:dependencies.bzl", "load_conan_dependencies") + load_conan_dependencies() + """) + + def setUp(self): + self.client = TestClient(path_with_spaces=False) # bazel doesn't work with paths with spaces + conanfile = textwrap.dedent(""" + from conans import ConanFile + from conans.tools import save + import os + class Pkg(ConanFile): + settings = "build_type" + def package(self): + save(os.path.join(self.package_folder, "include/hello.h"), + '''#include + void hello(){std::cout<< "Hello: %s" <