用bazel构建C ++

介绍与动机


最近,在Habr上出现了cmake和c ++是朋友的帖子,不仅提供了如何收集仅标头的库的示例,而且至少没有概述一些新的构建系统-bazel,buck,gn和其他。如果您像我一样,在2k20中用C ++编写,那么我建议您熟悉bazel作为c ++项目的构建系统。

我们将不再讨论cmake和其他现有系统的缺点,而是专注于bazel本身可以做什么。为了决定最适合您的是什么,我将其留给您。

让我们从定义和动机开始。Bazel是一种多语言Google构建系统,可以构建c ++项目。为什么我们还要看另一个构建系统?首先,因为她已经在进行一些大型项目,例如Tensorflow,Kubernetes和Gtest,因此要与它们集成,她已经需要能够使用bazel。其次,除了Google bazel之外,根据其在bazelcon上的表现来判断,它仍然使用spaceX,nvidia和其他公司。最后,bazel是github上一个非常受欢迎的开源项目,因此绝对值得一试并尝试一下。

例子1.琐碎的


有main.cc,需要编译:

main.cc

#include <iostream>

int main() {
   std::cout << "Hello, habr" << std::endl;
   return 0;
}

一切都始于工作空间声明。就bazel工作空间而言,这是所有源文件所在的目录。要指定此工作空间,您需要在我们需要的目录(通常是src目录)中创建一个名称为WORKSPACE的空文件。

在bazel中组织代码的最小单位是包装。该包由源目录和一个特殊的BUILD文件定义,该文件描述了如何组装这些源。

将主程序包添加到我们的项目中:



在BUILD文件中,我们现在必须描述要从主程序包构建的内容。当然,我们要编译可执行二进制文件,因此我们将使用cc_binary规则。 Bazel已经开箱即用地支持C ++,因此已经存在一些用于构建c ++目标的规则,我们将在以后了解其余内容。

将cc_binary规则添加到BUILD文件中,它的名称将包含一个可执行文件和一系列将传递给编译器的源。所有这些都在starlark中进行了描述,starlark是一个截断的python。

cc_binary(
  name = "hello_world",
  srcs = "main.cc"
)

Bazel与cmake不同,Bazel不是基于命令,而是允许通过规则以声明方式描述依赖关系。本质上,规则将多个工件与特定操作相关联。bazel使用它们来构建命令图,然后将其缓存并执行。在我们的例子中,main.cc源文件与一个编译操作相关联,其结果是工件hello_world-一个二进制可执行文件。

现在要获取可执行文件,我们必须转到带有工作空间的目录并键入:

bazel build //main:hello_world

构建系统从项目的根开始接受构建命令和实现目标的路径。

生成的二进制文件将位于bazel-bin / main / hello_world。

例子2.用你的库构建


幸运的是,没有人需要这么简单的项目,所以让我们看看如何为我们的项目添加功能。添加一个将单独构建并链接到我们的主库的库。

假设它是Square,它将提供明确的平方函数的库。添加新库意味着添加新包,我们也称其为正方形。





#ifndef SQUQRE_SQUARE_H_
#define SQUQRE_SQUARE_H_

int Square(int x);

#endif // SQUQRE_SQUARE_H_

方码
#include "square/square.h"

int Square(int x) {
  return x * x;
}

注意头文件的连接,尽管文件位于同一目录中,但我还是通过工作空间的路径来完成头文件的连接。铬代码样式指南中采用了这种方法,该指南继承自google c ++样式指南。此方法使您可以立即了解头文件的连接位置。不用担心,会有一个文件,bazel将添加搜索头文件的路径,但是如果您不遵循此规则,则在bazel构建过程中可能找不到头文件。

在我们库的BUILD文件中,我们描述了构建cc_library库的规则:

cc_library(
  name = "square",
  srcs = ["square.cc"],
  hdrs = ["square.h"],
  visibility = ["//visibility:public"]
)

在这里,我们分别列出了源文件和头文件,还指定了公共可见性。后者是必需的,以便我们可以在项目中的任何地方依赖我们的库。

在main.cc中,我们使用我们的库:

#include <iostream>
#include "square/square.h"

int main() {
  std::cout << "Hello, bazel!" << std::endl;
  std::cout << Square(2) << std::endl;
  return 0;
}

同样,我提请注意以下事实:我们通过工作空间的路径包含了库的头文件。这在这里已经是绝对必要的,因为bazel在引擎盖下使用Linux容器来确保最小程度的组装性,因此,它将安装方库头文件,以便它们从工作空间通过路径定位。

并且我们在正方形库中的main的组装规则中描述了依赖性。

cc_binary(
  name = "hello_world",
  srcs = ["main.cc"],
  deps = ["//square:square"]
)

我们整个程序的组装方式与不使用库的方式相同,bazel将了解它所依赖的内容,构建图形,缓存结果并仅重新组装需要重建的内容。

bazel build //main:hello_world

例子3.连接测试


没有测试如何生活?没门!要连接到batest GTest(已支持与bazel组装),您需要添加一个外部依赖项。这是在WORKSPACE文件中完成的:

load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")

git_repository(
  name = "googletest",
  remote = "https://github.com/google/googletest",
  tag = "release-1.8.1"
)

就像赶时髦的人一样,他们连接了git_repository规则,并告诉bazel要下载哪个版本。

接下来,我们为测试测试创建一个单独的程序包,并将测试添加到我们的库中:

square_unittest.cc

#include "gtest/gtest.h"
#include "square/square.h"

TEST(SquareTests, Basics) {
    EXPECT_EQ(Square(-1), 1);
    EXPECT_EQ(Square(1), 1);
    EXPECT_EQ(Square(2), 4);
}

现在该为测试定义一个规则了。

cc_test(
  name = "unittests",
  srcs = ["square_unittest.cc"],
  deps = [
   "//square:square",
   "@googletest//:gtest_main"
  ]
)

我们在库和gtest_main中添加了相关性,以便gtest库本身可以为我们提供启动器实现。

使用以下命令运行测试:

bazel test //test:unittests

Bazel将自己下载并构建GTest,链接测试所需的所有内容并自行运行测试。

我提到bazel也知道如何进行代码覆盖:

bazel coverage //test:unittests

而且,如果您需要调试测试,则可以使用以下字符在调试模式下编译所有内容:

bazel build //test:unittests --compilation_mode=dbg -s

例子4.连接不知道如何打包的其他库


当然,世界并不是仅基于bazel构建的,因此您还需要能够连接其他库。最近,在我的项目中,我需要一个库来解析命令行参数。好吧,不要在2k20里给我写你自己的这样的库,而从主要工作中分散注意力。我真的不想使用诸如getops之类的任何一半措施,也不想将boost拖入我的项目中。

不用于广告,我们将连接CLI11库,该库仅使用stl标准C ++ 11,并提供或多或少的方便界面。

库是仅标头的,这使得连接它特别容易。

在WORKSPACE中连接外部依赖项:

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")

http_file(
  name = "CLI11",
  downloaded_file_path = "CLI11.hpp",
  urls = ["https://github.com/CLIUtils/CLI11/releases/download/v1.9.0/CLI11.hpp"],
  sha256 = "6f0a1d8846ed7fa4c2b66da3eb252aa03d27170258df...",
)

我们添加了第三方目录并添加了CLI11包,以便于在此库上建立依赖关系:

cc_library(
  name = "CLI11",
  hdrs = ["@CLI11//file"],
  strip_include_prefix = "/external/CLI11/file",
  include_prefix = "CLI11",
  linkstatic = True,
  visibility = ["//visibility:public"],
)

Bazel默认情况下将通过路径/ external / CLI11查找库文件,以便我们稍微更改路径以通过CLI11 /连接它。

main.cc

#include <iostream>

#include "CLI11/CLI11.hpp"
#include "square/square.h"

int main() {
  std::cout << "Hello, bazel!" << std::endl;
  std::cout << Square(2) << std::endl;
  return 0;
}

根据主要情况,添加“ // third_party / CLI11:CLI11”,一切开始工作。
我不了解您,但是连接一些不熟悉的库并以这种形式在c ++项目中使用它会让我感到高兴。

是的,使用仅标头的库,您会说一切都很简单,但是对于尚未使用bazel构建的非标头的库,一切都一样简单。您只需通过http_archive或git_repository下载它,然后在第三方目录中向其添加外部BUILD文件,即可在其中描述如何构建库。Bazel支持通过cmake_external规则调用任何cmd,甚至调用cmake。

例子5.脚本和自动化


谁需要2k20中裸c ++的项目而没有自动化脚本?通常,需要这样的脚本来运行性能测试或将工件部署到CI。好吧,通常它们是用python编写的。

为此,bazel也适用,因为它几乎可以在所有流行语言中使用,并且旨在从实际项目中经常发现的不同编程语言中收集此类solyanka。

让我们连接将运行我们的main的python脚本。

添加perf-tests包:

py_binary(
  name = "perf_tests",
  srcs = ["perf_tests.py"],
  data = [
    "//main:hello_world",
  ],
)

作为数据依赖项,添加hello_world二进制依赖项。

perf_tests.py

import subprocess
import time

start_time = time.time()
process = subprocess.run(['main/hello_world, 'param1', ],
                         stdout=subprocess.PIPE,
                         universal_newlines=True)
end_time = time.time()
print("--- %s seconds ---" % (end_time - start_time))

为了运行我们的测试,我们只需编写:
bazel run //perf-tests:perf_tests

结论和未触及的内容


我们简要介绍了bazel及其主要功能,用于组装第三方和我们自己的可执行文件和库。就我的口味而言,结果非常简洁,迅速。无需受苦,无需寻找一些cmake教程即可完成一些琐碎的事情并清理CmakeCache。

如果您感兴趣,那么还有很多遗留的东西:连接协议缓冲区,清理程序,设置工具链以针对不同的平台/体系结构进行编译。

感谢您的阅读,希望对您有所帮助。

All Articles