2403-165-bazel的用法

1,238 阅读19分钟

摘要

本篇文章我介绍了bazel作为cpp一个构建工具的基本入门使用方法,介绍了有bazel构建工具的基本用法,bazel和cmake的对比,bazel的安装方法。bazel作为一个复杂的构建(不是编译)工具,我在本文中提到了很多它命令背后的一些行为,比如它的编译产出,bazel如何链接二进制库,bazel如何编译第三方库等。最后,我使用bazel发布自己的cpp代码库,zpplib。

bazel是什么

Bazel 是一个由 Google 开发的开源构建和测试工具,主要用于大规模和复杂的软件项目。Bazel 支持多种语言,并且能够跨平台工作。它使用一个称为 BUILD 文件的配置文件来指定项目的构建规则。

如果你还不理解bazel是什么,恰好你又是java程序员过来的,那么我告诉你,bazel是一个类似于maven的工具包,这样你也就理解了。

Bazel 的官方网站是 bazel.build。在这个网站上,你可以找到关于 Bazel 的文档、教程、最佳实践、以及如何开始使用 Bazel 的指南。此外,网站还提供了关于 Bazel 的最新新闻、发布信息和社区资源。

以下是使用 Bazel 的一些基本步骤:

  1. 安装 Bazel

    • 在 Windows 上,你可以下载安装包并按照指示进行安装。
    • 在 Linux 或 macOS 上,你可以使用包管理器或从官方 GitHub 仓库安装。
  2. 设置工作区

    • 创建一个目录作为你的工作区。
    • 在工作区目录中,创建一个名为 WORKSPACE 的空文件,这告诉 Bazel 这个目录是工作区的根目录。
  3. 编写 BUILD 文件

    • 在工作区目录中或其子目录中创建 BUILD 文件,用于定义构建规则。
    • 在 BUILD 文件中,你可以定义源文件、依赖关系和构建目标。
  4. 运行 Bazel 命令

    • 在工作区目录中,你可以运行 bazel build 命令来构建你的项目,构建完成后,你的工作区下会有个文件夹叫做 bazel-bin,那里面有二进制文件。
    • 使用 bazel test 命令来运行测试。
    • 使用 bazel run 命令来运行可执行文件。
  5. 使用 Bazel 构建规则

    • Bazel 支持多种语言的构建规则,例如 Java、C++、Python 等。

      一般来说,bazel太复杂了,每个语言都有自己的开源构建工具,比如java有 gradle和maven, 而且这种构建工具复杂的不是一点点,语法学习成本不小,虽然他们的本质都是代替人手工的过程,但是确实很复杂,让人讨厌!所以我想我目前只会用它编译C++。

    • 你可以自定义构建规则来满足特定需求。

  6. 依赖管理

    • Bazel 支持外部依赖,你可以通过在 WORKSPACE 文件中添加外部依赖的规则来引入它们。
  7. 查询和检查构建结果

    • 使用 bazel query 命令来查询有关构建目标的信息。
    • 使用 bazel analyze 命令来分析构建的性能。

Bazel 的使用可能会根据项目的具体需求和复杂性而有所不同。对于大型项目,可能需要编写复杂的 BUILD 文件和自定义规则。建议查阅 Bazel 的官方文档来获取更详细的信息和教程。

入门案例

Hello world

假设你已经安装了 Bazel。

  1. 创建工作区目录

    mkdir my_workspace
    cd my_workspace
    touch WORKSPACE
    
  2. 编写 C++ 程序: 创建一个名为 main.cpp 的文件,并写入以下内容:

    #include <iostream>
    
    int main() {
        std::cout << "Hello, World!" << std::endl;
        return 0;
    }
    
  3. 创建 BUILD 文件: 在工作区目录中创建一个名为 BUILD 的文件,并写入以下内容:

    cc_binary(
        name = "hello_world",
        srcs = ["main.cpp"],
    )
    
  4. 构建项目: 在工作区目录中运行以下命令来构建项目:

    bazel build //:hello_world # 注意, //不表示这是注释,而是一个路径。
    
  5. 运行程序: 构建完成后,你可以使用以下命令来运行程序:

    bazel run //:hello_world
    

当你运行这个命令时,你应该会在终端看到输出 “Hello, World!”。

这个案例非常简单,但它展示了如何使用 Bazel 来构建和运行一个基本的 C++ 程序。对于更复杂的项目,你可能需要添加更多的源文件、依赖关系和构建规则。

路径表示

在 Bazel 中,//:target_name 是一种标签表示法,用于指定构建目标。这种表示法中的各个部分含义如下:

  • //:表示当前工作区的根目录。// 是所有相对标签的起点。

  • ::用作分隔符,它将工作区路径与目标名称分开。

    补充一个事实:不要冒号行不行呢?

    ":",也就是冒号,隔离了路径和最终的编译目标。 如果 //happy 和 //:happy,如果没有冒号,happy可能就是一个路径。bazel会试图检查 happy是文件夹还是文件夹里的编译目标,模糊的东西会降低效率

  • target_name:是在 BUILD 文件中定义的具体构建目标的名称。

例如,//:hello_world 表示在根目录(/)的 BUILD 文件中定义的名为 hello_world 的构建目标。如果你有一个不同的目录结构,并且想要指定子目录中的目标,你可以使用类似于 //path/to/subdirectory:target_name 的标签。

这里是一些例子:

  • //:hello_world:根目录中的 hello_world 目标。
  • //path/to:targetpath/to 子目录中的 target 目标。
  • //path/to/subdirectory:targetpath/to/subdirectory 子目录中的 target 目标。

使用这种表示法,Bazel 能够准确地找到你想要构建、测试或运行的特定目标。

安装bazel

因为我是macos, 所以我必须选择对应的平台。为了每次都很不方便的安装,我选择把我的安装自动化,写成了一个脚本。

# 安装bazel

export BAZEL_VERSION=5.2.0
curl -fLO "https://github.com/bazelbuild/bazel/releases/download/$BAZEL_VERSION/bazel-$BAZEL_VERSION-installer-darwin-x86_64.sh"


chmod +x "bazel-$BAZEL_VERSION-installer-darwin-x86_64.sh"
./bazel-$BAZEL_VERSION-installer-darwin-x86_64.sh --user

rm bazel-$BAZEL_VERSION-installer-darwin-x86_64.sh

这段代码被保存在 bazel-macos.dev.sh · notfresh/zservices -…,它隶属于一个项目 zservices, 地址是gitee.com/notfresh/zs…

我是这么介绍这个项目的:这个仓库可以一行命令安装很多软件,包括docker, java, go等等。 具体的方法是把安装过程写成shell脚本。好处是可以记录软件的版本等等。

我写这个项目的目的在于:

  1. 厌倦了各种平台安装软件的麻烦,比如一会使用 MacOS平台的brew,一会使用Ubuntu的 apt, 使用centos的 yum, 我决定直接把它做成一个基于官方发布的安装工具的东西。
  2. 我需要经常搭建基础开发环境,把一个刚买来的云主机快速变成一个可供远程开发的主机,所以我需要这些工具。
  3. 为了让这些工具足够简单好用、可控,同时熟练掌握 shell,我于是把安装的过程固化了下来,一次脚本终身受益,希望也能帮到你。
  4. 国内的下载加速。很多著名软件都是托管在国外的服务器上,下载速度你懂的。

如果它真的帮助到你,希望你能给个star或者给它贡献代码。

关于bazel和cmake的考量

bazel是一种类似于 Java世界maven的角色,然而C++世界不止这么一种编译工具,包括cmake。

cmake语法很奇怪,我学了很久,后来发现这东西作为一个工具还是别钻太深,没有什么用,不要浪费很多时间,看了一篇文章【参考2】,我把主要内容列举出来。

优势

Bazel存在如下方面的优势。

  • 易理解:Bazel对外提供声明式表达的构建DSL,可读性好,降低了构建系统的实现复杂度;
  • 速度快:得益于Bazel优异的编译缓存和依赖分析技术,更好地支持并行和增量编译,甚至增量测试;
  • 跨平台:一套构建系统,支持多平台,异构系统的构建;
  • 可扩展:使用类Python语言(Starlark)扩展规则,支持多语言构建;
  • 大规模:支持100k+源文件规模的构建,支持多仓的依赖管理;
  • 云构建:天然地支持云构建,复用既有的计算资源。

举个例子,使用Bazel,工程中如果需要自动生成Protobuf代码,配置CUDA编程环境,管理多仓代码的依赖等,Bazel相对具有明显的优势。

劣势

但是,Bazel也存在一些与生俱来的缺陷。

Bazel运行于JRE

也就是说,安装Bazel需要额外安装JRE,即使Bazel二进制包内嵌了一个最小化的JRE;此外,Bazel启动速度迟钝,内存消耗很厉害。

业界存在其他构建工具,例如Scons,它使用Python。但是,在几乎所有的Unix开发环境中,Python是预装的,这给开发者减少了很大麻烦。

所以,Native风格的构建工具具有很大的竞争优势。一方面,安装极简,用户心智负担不大;另一方面,用户不需要额外安装依赖,整个工具链的安装是完备的、闭包的。

Bazel与Unix社区文化存在冲突

在Unix社区,开发者已然非常熟悉./configure, make, make install的构建工作流,及其相关的工具链。而且,开发者一致遵循FHS(文件系统层次标准)的约定。

但是,Bazel改变了Unix一贯的风格和文化,与社区文化产生了剧烈的矛盾。如果你使用Bazel发布一个库,对不起,你的用户也得用Bazel,才能引用到你的库。Bazel不支持类似于bazel install命令,你的用户只能使用Bazel,然后创建新的规则去依赖你的库。使用Bazel,你强奸了你的用户群。

反过来,如果Bazel要想依赖于一个非Bazel的库,得做适配才能使用,用户不能复用既有存在的CMake或Make的构建系统。众所周知,有的构建系统异常复杂,适配过程并非易事,你得对库的架构,依赖关系把握非常准确才行。Bazel开发团队也没有提供相关工具链,翻译既有存在构建脚本,这个过程留待给用户自行解决。

总结起来,Bazel到Make,不支持;从Make到Bazel,得出血;从Bazel到Bazel,零成本,但可能招致用户的强制性。这对社区文化是一种伤害,这也是Bazel最大的硬伤。如果没有背后有钱的老爹Google,估计Bazel早死了。

这可能与Bazel的定位和架构有关系,大家也不能骂爹骂娘骂Google傻逼。Bazel定位在于支持多语言,而不仅仅只包括C/C++,C/C++的一些惯用法和工具链,在其他语言可能不成立。

此外,/usr/include, /usr/lib是系统目录。如果你存在两个工程,分别依赖于相同的、当版本不同的库。它们都安装到系统目录,必然存在版本冲突。幸运的是,容器时代这个问题得到了缓解。相反地,在每个Bazel项目中,将其依赖控制在自己的工作区(Workspace)内,避免了类似的版本冲突问题。

另外,Bazel的霸道,也复合Google一贯高傲的风格。通过Bazel的黏性,Google在多语言多语言编程领域,尤其在云构建领域,正在积极构建强大的生态系统和技术壁垒。

Bazel的生态系统不够成熟

在C/C++领域,CMake,Make占据主流。Bazel作为后起之秀,能否在生态中站稳脚跟,还得靠时间证明。此外,上文提及Bazel与社区文化存在冲突和矛盾,Bazel的生态建设还是不够乐观。

目前,整个Bazel的生态,基本由TensorFlow社区挑大梁。但是,TensorFlow的最佳实践,也很难在社区中得到有效的传播和复制。而且,TensorFlow在实践Bazel也遇到了一些挑战,包括复杂度(构建脚本代码行,及其依赖的复杂度)。

一方面,TensorFlow的系统架构和实现存在固有的复杂度;因为TensorFlow是多语言、异构的系统实现,常见的工程的构建过程不见得拥有TensorFlow那么复杂,而且大部分公司的工程也不见得拥有Google的复杂度。在Google玩得转的技术实践,在其他公司并不一定有效。

另一方面,抢占既有存在的生态系统,本身门槛极高。犹如在Java领域,个人认为Gradle比Maven优秀,但Gradle的生态依然没有Maven健全和完善。从社区活跃度看,目前只有Google相关的项目在推进Bazel,其他项目几乎没什么动静,由此可见一斑。

Monorepo并非是万能的

Bazel的架构思维是Monorepo哲学的技术延伸。但是,Monorepo是否有效,要取决于公司的文化,团队协助方式,项目特点等众多因素,在日常的项目中,并非一定是灵丹妙药。采用Monorepo组织项目,Git库变得越来越大,甚至对IDE也提出了挑战;更有甚者,Monorepo往往导致模块之间的依赖隐式化,不能得到及时的显性暴露,增加了系统架构的耦合度。

bazel和其他包管理工具对比

Bazel 和传统的包管理工具(如 npm、pip、apt-get、yum 等)在设计和用途上有一些显著的区别。以下是它们之间的一些主要差异:

  1. 目的和范围
    • Bazel 是一个构建工具,它的主要目的是编译和测试软件。它支持多种编程语言和平台,并提供了高度可配置和可扩展的构建规则。
    • 包管理工具主要用于安装、升级和管理软件包。它们通常与特定的编程语言或操作系统绑定,例如 npm 用于 Node.js,pip 用于 Python,apt-get 用于 Debian/Ubuntu 系统。
  2. 依赖管理
    • Bazel 通过源代码的方式来管理依赖。它通常不直接处理预编译的二进制包,而是从源代码开始构建,确保构建的可重现性和一致性

      我个人认为这是至关重要的一个不同之处。bazel倾向于通过源码编译。

    • 包管理工具通常处理预编译的二进制包或源代码包。它们会下载并安装这些包到系统的特定位置,并提供版本控制和依赖解析功能。

  3. 构建过程
    • Bazel 提供了细粒度的控制,允许开发者定义复杂的构建规则和依赖关系。它支持增量构建和并行任务执行,以优化构建性能。
    • 包管理工具通常不涉及构建过程,它们主要负责安装已经构建好的软件包。一些包管理工具可能提供了简单的构建功能,但这些通常不如 Bazel 这样的专业构建工具强大。
  4. 跨平台和一致性
    • Bazel 被设计为跨平台工作,可以在不同的操作系统上提供相同的构建结果,这对于大型项目和团队协作尤为重要。
    • 包管理工具通常与特定的操作系统或语言生态系统紧密相关,它们在不同的平台上可能会有不同的行为或依赖。
  5. 缓存和分发
    • Bazel 强调构建缓存的使用,可以显著减少重复构建的时间。它还支持分布式构建,可以在多个机器上并行执行构建任务。
    • 包管理工具可能提供缓存机制来加速软件包的下载和安装,但它们通常不涉及构建缓存的概念。
  6. 配置和自定义
    • Bazel 允许开发者通过编写自定义构建规则(如 Skylark/Starlark)来扩展其功能,以满足特定的构建需求。
    • 包管理工具通常提供配置文件来指定依赖和安装选项,但它们的自定义能力通常不如 Bazel。 总结来说,Bazel 是一个专注于构建和测试过程的工具,它提供了细粒度的控制和高度的可配置性,以确保构建的一致性和性能。而包管理工具主要关注于软件包的安装和管理,它们简化了软件的获取和部署过程,但不涉及源代码的编译。在实际的开发流程中,Bazel 和包管理工具可以互补使用,以实现从源代码到部署的完整工作流程。

高级用法和知识

第三方库

我创建了一个库,叫做 zpplib,我以此作为例子来说明如何使用第三方依赖。

zpplib的地址是 github.com/notfresh/zp…

zpplib本身依赖了 google家的abseil库,它也可以被分发给其他项目来使用,下面开始说明:

远程安装

zpplib怎么使用abseil库呢?最好像java中的maven工具一样,直接三行代码就可以用。

幸运的是 bazel 提供了这样的功能。

在 zpplib的 根目录下,有一个 WORKSPACE文件,这个文件指定了本项目需要依赖的其他项目。

**远程安装需要使用http_archive这个工具。使用第三方依赖时,需要发布方打包好源码,制作成压缩包,放在一个可以被下载的平台。**更新了代码之后,需要重新打包并且发布版本。

使用abseil库

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

http_archive(
  name = "com_google_absl",
  urls = ["https://github.com/abseil/abseil-cpp/archive/98eb410c93ad059f9bba1bf43f5bb916fc92a5ea.zip"],
  strip_prefix = "abseil-cpp-98eb410c93ad059f9bba1bf43f5bb916fc92a5ea",
)

简单解释一下几个命令:

load表示加载一个插件,这里用到的是 http_archive

在http_archive命令里, name字段是你给远程依赖起的名字,一般和官方保持一致即可,当然你也可以起一个自己喜欢的,但是后面保持一致。

urls命令表示下载的地址,这个地址 github.com/abseil/abse… 就是 abseil库的在线地址,你使用浏览器也可以保存到本地,迅雷也可以,使用 wget或者 curl也可以。这个文件实际上下载下来,名字叫 abseil-cpp-98eb410c93ad059f9bba1bf43f5bb916fc92a5ea.zip, 而不是98eb410c93ad059f9bba1bf43f5bb916fc92a5ea.zip, 所以你需要知道的是 网址字符不等于实际文件名。

strip_prefix 是什么意思呢?我觉得这个比较费解,我着重解释一下。 字面意思来看,strip prefix, 去掉前缀。

为什么要去掉前缀?

这需要看一下这个代码包的结构和abseil库的结构。如果你把abseil-cpp-98eb410c93ad059f9bba1bf43f5bb916fc92a5ea.zip, 直接解压,它的内部目录就跟 github代码一致,源码而非二进制。

类似于下面这样:

➜  com_google_absl: tree -L 1
.
├── ABSEIL_ISSUE_TEMPLATE.md
├── AUTHORS
├── CMake
├── CMakeLists.txt
├── CONTRIBUTING.md
├── LICENSE
├── LTS.md
├── README.md
├── UPGRADES.md
├── WORKSPACE
├── absl
├── ci
└── conanfile.py

指定strip_prefix就是消除掉库的文件夹名,但是具体为什么?TODO,我也没搞清楚现在。这就是构建工具复杂的地方!

然后你在任何一个需要abseil库地方引用即可。比如下面的:

cc_binary(
  name = "hello",
  deps = ["@com_google_absl//absl/strings"],
  srcs = ["hello.cpp"],
)

hello.cpp代码如下:

/**
 * @file hello.cpp
 * 
 * 参考 https://abseil.io/docs/cpp/quickstart
 * 
 * @author zhengxu  
 * @brief 
 * @version 0.1
 * @date 2024-04-03
 * 
 * @copyright Copyright (c) 2024
 * 
 */
#include <iostream>
#include <string>
#include <vector>

#include "absl/strings/str_join.h"

int main() {
  std::vector<std::string> v = {"foo", "bar", "baz"};
  std::string s = absl::StrJoin(v, "-");

  std::cout << "Joined string: " << s << "\n";

  return 0;
}

使用 bazel run //:hello 即可运行代码。

使用zpplib库

zpplib的代码结构如下:

➜  zpplib git:(main) tree -L 2
.
├── Makefile
├── README.en_US.md
├── README.md
├── WORKSPACE
├── bazel-bin -> /private/var/tmp/_bazel_zxzx/bfeea68c00b65e069033dbfa4c082e14/execroot/person_notfresh_zpplib/bazel-out/darwin-fastbuild/bin
├── dev.md
├── release-zip.sh
├── zpplib
│   ├── BUILD
│   ├── hello.cpp
│   ├── main.cpp
│   ├── math.h
│   ├── sort.h
│   └── tree.h
└── zpplib-release-v0.1.zip

如果你想使用zpplib库,可以在WORKSPACE这样写:

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

http_archive(
  name = "zpplib",
  urls = ["https://github.com/notfresh/zpplib/releases/download/v0.1/zpplib-release-v0.1.zip"],
)

这里没有指定 strip_prefix 字段。

使用zpplib,可以在一个BUILD文件里写明类似于这样的代码:

cc_binary(
    name = "main",
    srcs = ["main.cpp"],
    deps = ["@zpplib//zpplib:math"],
)

main.cpp代码如下:

#include <iostream>
#include "zpplib/math.h"

using namespace std;

int main() {
    cout << "Hello, world!" << endl;
    cout << greater_(1,2)  << endl;
    cout << little_(1,2)  << endl;
    return 0;
}

本地安装

如果你已经在本地下载好了zpplib,就像我自己在本机开发完成,在另个仓库测试引用 zpplib,我可以在WORKSPACE这么写:

local_repository(
    name = "zpplib",
    path = "/Users/zxzx/projects/code/cpp/zpplib",
)

第三方库的编译

在 Bazel 中,第三方库的编译通常发生在以下情况下:

  1. 首次构建:当你第一次构建你的项目时,Bazel 会检查项目的依赖关系,并发现它需要编译第三方库。此时,Bazel 会根据 WORKSPACE 文件中的规则下载(如果尚未下载)第三方库的源代码,并按照 BUILD 文件中的规则进行编译。bazel只会编译需要的部分库,比如abseil库内容很多,但是string是一个单独的模块,你值引用了abseil的string模块,那么bazel只会编译你需要的string模块和其依赖的模块。
  2. 依赖变化:如果你的项目中的任何文件或第三方库的源代码发生了变化,Bazel 会检测到这些变化,并重新编译受影响的依赖项。这种变化可能包括源文件的修改、头文件的更新或者构建规则的改变。
  3. 构建命令指定:在某些情况下,即使没有发生任何变化,你也可以通过 Bazel 命令显式地要求重新编译第三方库。例如,使用 bazel build --fetch 可以强制 Bazel 重新下载和构建所有外部依赖。
  4. 缓存失效:Bazel 使用缓存来加速构建过程。如果缓存中的第三方库构建结果被标记为无效(例如,由于缓存策略的改变或者手动清除缓存),Bazel 将重新编译这些库。
  5. 目标依赖:如果你构建的项目中的一个目标(如可执行文件或测试)依赖于第三方库,并且该目标的构建触发时,Bazel 会确保所有依赖的库都是最新编译的。 Bazel 的构建系统是高度优化的,它会尽可能地重用已经编译的库,以减少不必要的编译工作。Bazel 通过其依赖关系图来跟踪哪些库需要重新编译,并仅在必要时才执行编译操作。这种增量构建的特性使得 Bazel 非常适合大型项目和团队协作开发。

链接已有的库

在 Bazel 中,如果你想直接链接已经编译好的库文件(例如,静态库 .a 文件或动态库 .so 文件),你可以使用 cc_library 规则的 srcs 属性来指定这些库文件。以下是一个基本的示例,展示了如何链接一个已经编译好的静态库:

  1. 准备库文件:确保你的库文件(例如 libmylibrary.a)已经准备好,并且位于你的项目目录中或者可以通过其他方式获取。

  2. 创建 BUILD 文件:在你的 BUILD 文件中,使用 cc_library 规则来指定你的库文件。例如:

    cc_library(
        name = "mylibrary",
        srcs = ["libmylibrary.a"],
        includes = ["path/to/library/headers"], # 如果需要包含头文件目录
        hdrs = glob(["path/to/library/headers/*.h"]), # 如果需要包含特定的头文件
    )
    

    如果你的库是动态库(.so 文件),你需要在 cc_library 规则中添加 linkshared = 1 参数:

    cc_library(
        name = "mylibrary",
        srcs = ["libmylibrary.so"],
        includes = ["path/to/library/headers"],
        hdrs = glob(["path/to/library/headers/*.h"]),
        linkshared = 1,
    )
    
  3. 链接到目标:在你的目标(例如可执行文件或另一个库)的 cc_librarycc_binary 规则中,将你的库作为依赖添加:

    cc_binary(
        name = "myprogram",
        srcs = ["myprogram.cc"],
        deps = [
            "@mylibrary//:mylibrary",
        ],
    )
    
  4. 构建项目:运行 Bazel 构建命令来构建你的项目:

    bazel build //:myprogram
    

    在这个例子中,myprogram 目标将链接到 mylibrary,后者直接使用了已经编译好的 libmylibrary.alibmylibrary.so 库文件。 请注意,如果你的库文件位于外部仓库或者需要特殊处理,你可能需要在 WORKSPACE 文件中使用 http_archive 或其他规则来下载或获取库文件,并在 BUILD 文件中正确引用它们。

安装二进制

要将 Bazel 编译好的可执行二进制文件放到操作系统的可执行路径下,你可以手动复制文件或者使用 Bazel 提供的安装规则。以下是一些常见的方法:

手动复制

  1. 找到二进制文件:首先,你需要找到 Bazel 编译好的二进制文件。这通常位于 bazel-bin 目录下,例如 bazel-bin/path/to/your/target/your_executable
  2. 复制到可执行路径:然后,你可以手动将这个二进制文件复制到你的操作系统的可执行路径下。在 Unix-like 系统中,这通常是 /usr/local/bin,而在 Windows 系统中,可能是 C:\Program Files\YourApp 或其他路径。
    cp bazel-bin/path/to/your/target/your_executable /usr/local/bin/
    
  3. 设置权限:在 Unix-like 系统中,你可能需要设置二进制文件的执行权限:
    chmod +x /usr/local/bin/your_executable
    

使用 Bazel 安装规则

Bazel 提供了 install 规则,可以帮助你将构建产物安装到指定的目录。你可以创建一个新的 BUILD 文件,用于定义安装规则,或者在你的现有 BUILD 文件中添加安装规则。 例如,你可以创建一个 install.sh 脚本,用于将二进制文件复制到目标目录:

#!/bin/bash
set -e
# 获取二进制文件的路径
BAZEL_BIN=$(bazel info bazel-bin)
EXECUTABLE_PATH="${BAZEL_BIN}/path/to/your/target/your_executable"
# 安装目录,例如 /usr/local/bin 或其他
INSTALL_DIR="/usr/local/bin"
# 复制二进制文件
cp "${EXECUTABLE_PATH}" "${INSTALL_DIR}/"
# 设置执行权限
chmod +x "${INSTALL_DIR}/your_executable"
echo "Installation complete."

然后,你可以在 BUILD 文件中创建一个 sh_binary 规则来定义这个脚本:

sh_binary(
    name = "install",
    srcs = ["install.sh"],
    data = [
        "//path/to/your/target:your_executable",
    ],
)

运行安装脚本:

bazel run //:install

请注意,将文件复制到系统目录可能需要管理员权限。在 Unix-like 系统中,你可能需要使用 sudo 来执行这些命令。 此外,如果你正在开发一个需要分发和安装的应用程序,你可能想要考虑使用包管理工具,如 dpkg(Debian 系统中使用),rpm(Red Hat 系统中使用),或者 homebrew(macOS 系统中使用),这些工具可以更方便地管理和安装软件包。

编译产出

在 Bazel 中,编译后的第三方库和其它构建产物通常会被放置在 Bazel 的输出目录中。默认情况下,这个目录是 bazel-bin。Bazel 会为每个目标和每个配置(例如,Debug 和 Release)创建一个对应的子目录。 例如,如果你编译了一个名为 my_project 的 C++ 库,并且使用了默认的构建配置,你可以在以下路径中找到编译后的库文件:

bazel-bin/my_project/libmy_project.so  # 对于动态库
bazel-bin/my_project/libmy_project.a   # 对于静态库

对于第三方库,Bazel 会遵循相同的规则。例如,如果你使用了 Abseil 库,并且编译了其中的 absl::strings 组件,你可以在类似以下路径中找到编译后的 Abseil 库文件:

bazel-bin/external/abseil/absl/strings/libabsl_strings.so  # 对于动态库
bazel-bin/external/abseil/absl/strings/libabsl_strings.a   # 对于静态库

这里的 external 目录是 Bazel 用于存放所有外部依赖的默认位置。每个外部依赖都会在其自己的子目录中创建,通常是按照 WORKSPACE 文件中定义的名称来命名。 请注意,Bazel 的输出目录结构可能会因为项目的配置和构建规则的不同而有所变化。你可以通过运行 bazel info 命令来获取更多关于 Bazel 输出目录的信息。此外,Bazel 还提供了其他目录,如 bazel-genfilesbazel-out,用于存放不同类型的构建产物和中间文件。

头文件怎么办?

在 Bazel 中,编译后的头文件通常不会单独生成,因为头文件(.h 文件)通常是源代码的一部分,并不需要编译。头文件被包含在源文件(.cc 或 .cpp 文件)中,在编译过程中直接使用。 然而,Bazel 确实会处理头文件包含路径(include paths),确保编译器能够找到所需的头文件。当你在 BUILD 文件中定义一个 cc_library 或其他 C/C++ 相关规则时,你可以使用 includes 属性来指定头文件的目录。例如:

cc_library(
    name = "my_library",
    srcs = ["my_library.cc"],
    hdrs = ["my_library.h"],
    includes = ["."],  # 当前目录
)

在这个例子中,my_library.h 是头文件,includes = ["."] 表示头文件位于与 BUILD 文件相同的目录中。这样,任何依赖于 my_library 的目标都会自动获得正确的头文件包含路径。 如果你需要将头文件复制到某个特定的输出目录,你可以使用 Bazel 的 filegroupcopy 规则来实现。例如:

filegroup(
    name = "my_headers",
    srcs = glob(["*.h"]),
)
copy_to_bin(
    name = "copy_headers",
    srcs = [":my_headers"],
)

在这个例子中,copy_to_bin 是一个自定义规则,用于将头文件复制到 bazel-bin 目录下。这样,你可以在构建过程中方便地访问这些头文件。 请注意,Bazel 的设计目标是保持构建的透明性和可重现性,因此它通常不会修改源代码文件,包括头文件。头文件通常直接从源代码树中使用,而不需要单独的编译步骤。

项目结构

在使用 Bazel 构建项目时,合理的代码结构对于确保构建的可维护性和效率至关重要。以下是一些基于 Bazel 的代码组织最佳实践:

  1. WORKSPACE 文件:项目的根目录应该包含一个 WORKSPACE 文件,用于定义项目的外部依赖。这个文件由 Bazel 自动识别,作为项目根目录的标志。
  2. BUILD 文件:每个包含构建目标的目录都应该有一个 BUILD 文件。BUILD 文件定义了如何构建该目录中的目标(如可执行文件、库、测试等)。建议将相关的源文件和 BUILD 文件放在同一个目录中。
  3. 目录结构:按照功能或组件组织代码目录。例如,可以将相关的源文件、头文件和测试放在同一个目录中。这种结构有助于保持代码的模块化和可读性。
  4. 层次化依赖:确保目录结构反映了目标之间的依赖关系。高层级的目录应该依赖于低层级的目录,而不是相反。这有助于避免循环依赖和保持清晰的依赖关系图。
  5. 外部依赖:使用 http_archive 或其他 Bazel 规则来管理外部依赖。在 WORKSPACE 文件中声明这些依赖,并在 BUILD 文件中引用它们。
  6. 库和二进制文件分离:将库代码和可执行文件代码分开。这样可以更容易地重用库代码,并减少构建时间,因为库只需要构建一次即可被多个可执行文件使用。
  7. 测试:为每个组件编写测试,并将测试代码与生产代码放在同一目录中。使用 Bazel 的测试规则(如 cc_test)来定义和运行测试。
  8. 构建脚本:如果需要,可以编写顶层的构建脚本(如 build.shbuild.bat),用于简化常见的构建任务。这些脚本可以调用 Bazel 命令来构建指定的目标。
  9. 文档:在项目中包含 README 文件和/或其他文档,说明如何构建和运行项目,以及如何使用项目中的不同组件。
  10. 版本控制:使用版本控制系统(如 Git)来管理代码和依赖。确保 WORKSPACE 文件和 BUILD 文件都受到版本控制。
  11. 一致性:在整个项目中保持一致的编码和构建风格。这有助于新成员快速熟悉项目,并减少错误。
  12. 性能优化:利用 Bazel 的缓存和增量构建特性来优化构建性能。避免不必要的重新构建,确保构建速度快且可靠。 通过遵循这些最佳实践,你可以创建一个结构清晰、易于维护和高效构建的 Bazel 项目。

bazel运行build子命令的背后

当你使用 Bazel 运行 build 命令时,Bazel 会执行一系列步骤来编译你的代码。以下是 Bazel 执行构建过程的一般概述:

  1. 解析构建文件:Bazel 首先解析项目中的 BUILD 文件和 WORKSPACE 文件。这些文件定义了项目的依赖关系、源文件、编译选项和其他构建规则。
  2. 分析依赖关系:Bazel 会分析你的项目中的目标和它们之间的依赖关系,构建一个依赖关系图。这个图决定了构建的顺序和需要重新构建的内容。
  3. 加载和分析外部依赖:如果项目依赖于外部库,Bazel 会根据 WORKSPACE 文件中的规则加载这些外部依赖。这可能包括从远程仓库下载依赖的源代码。
  4. 执行编译命令:Bazel 会为每个目标生成编译命令。对于 C++ 项目,这可能包括调用 g++ 或 clang++ 编译器。Bazel 会利用并行和增量构建来优化构建过程,只重新编译那些直接或间接依赖于已更改文件的代码。
  5. 构建中间产物:在编译过程中,Bazel 会生成中间产物,如对象文件(.o 文件)和库文件。这些中间产物通常存储在 Bazel 的输出目录中,默认为 bazel-bin
  6. 链接:如果构建的目标是一个可执行文件或共享库,Bazel 会将所有必要的对象文件和库链接在一起,生成最终的可执行文件或库文件。
  7. 输出结果:构建完成后,Bazel 会将生成的可执行文件、库文件和其他输出结果放置在输出目录中。你可以使用 bazel-bin 目录来找到这些文件。
  8. 缓存和优化:Bazel 会缓存构建结果,以便在未来的构建中重用。如果没有任何源文件或依赖关系发生变化,Bazel 可以从缓存中直接获取结果,从而加快构建速度。 Bazel 的构建过程是高度优化和可扩展的。它支持多种编程语言和平台,并且可以通过编写自定义规则来扩展其功能。Bazel 的设计目标是确保构建结果是可重现的,这意味着在不同的系统和时间点上,构建相同版本的代码应该产生相同的结果。

使用bazel发布自己的包

Bazel

  • 如果你已经使用 Bazel 作为你的构建系统,你也可以用它来分发你的库。
  • Bazel 支持跨平台构建,并且可以确保构建的一致性和可重现性。
  • 你可以提供 Bazel 的构建文件(BUILD),让用户可以直接使用 Bazel 来构建你的库。

光使用别人的包没有意思,重要的在于作为一个生产者,发布自己的包。

然而如何基于bazel发布自己的包的细节,这是个问题。

参考abseil

然而这个问题一点都不好解决,当我视图参考 abseil_cpp库的时候,发现它有很多奇怪的语法和高级的功能,应该是为了解决各种平台的问题。

于是我去问了 清华大模型。

清华大模型的说法

在 Bazel 中,http_archive 是一个规则,用于从远程 URL 下载和缓存依赖项的源代码或预构建的二进制文件。如果你想制作一个 http_archive 依赖项,以便别人可以使用它,你需要遵循以下步骤:

  1. 选择一个远程仓库
    • 选择一个合适的远程仓库,例如 GitHub、GitLab 或 Bitbucket。
    • 确保仓库中的代码是结构良好的,并且符合 Bazel 的规则和约定。
  2. 创建一个版本标签
    • 在 Git 仓库中,选择一个 commit,它代表了你想要发布的依赖项的版本。
    • 创建一个新的 tag,格式通常是 v<version>,例如 v1.0.0
  3. 构建依赖项
    • 在远程仓库中,构建你的依赖项。
    • 确保构建成功,并且所有依赖项都被正确引入。
  4. 打包依赖项
    • 根据你的依赖项的性质,你可能需要打包一些资产,如二进制文件、库文件等。
    • 确保这些文件都放置在正确的位置,并且可以被用户轻松下载和使用。
  5. 上传资产到远程仓库
    • 在远程仓库中,创建一个新的 release。
    • 选择你刚刚创建的 tag。
    • 添加资产文件,这些文件将作为下载链接提供给用户。
    • 填写 release 描述,包括版本信息、更新日志和安装说明。
  6. 创建 http_archive 规则
    • 在你的 Bazel 项目的 WORKSPACE 文件中,创建一个 http_archive 规则。
    • 指定远程仓库的 URL、标签、校验和等信息。
    • 确保规则的名称是唯一的,并且不会与项目中的其他依赖项冲突。
  7. 发布依赖项
    • 一旦发布,你可以通过邮件列表、Slack 频道、Twitter 等渠道通知社区。
  8. 更新文档和 README
    • 更新你的 README 文件和文档,包括如何使用你的依赖项的说明,以及指向最新 release 的链接。 请注意,发布基于 Bazel 的依赖项的步骤可能会根据你使用的代码托管平台和 Bazel 项目的具体结构有所不同。确保遵循你所在社区的最佳实践和指导原则。

分发自己的代码

有几个问题是,怎么打包?哪些排除在外?

http_archive 规则又是什么?

说实话,写到这里,已经很挫折了。这个工具真的是相当的陡峭啊,非常的不友好啊,没有傻瓜式的操作,就只能靠自己摸索了,文档写的非常的迷惑。

cpp能干什么?为什么要靠这个工具受罪呢?

难道我不能使用最简单的make工具吗?

第一遍是痛苦的,但是走过来了。 我描述一下如何分发 zpplib的。

zpplib的代码在 github.com/notfresh/zp…

写一个打包脚本,比如 release-zip.sh

zip -r zpplib-release-v0.1.zip .  \
 -x "bazel-zpplib/*"  -x "bazel-bin/*" -x "bazel-out/*" -x ".git/*" \
 -x "bazel-testlogs/*"

然后,我们需要打包仓库源代码,在这里,运行这个脚本即可。

然后把打包好的代码手动上传到github上的仓库对应的release页面去。

release功能的入口在仓库项目首页的右侧,点击即可进入。

其他分发思路

如果你想分发自己的 C++ 库,有几种工具和方法可供选择,具体取决于你的目标和需求。以下是一些流行的选项:

  1. CMake
    • CMake 是一个跨平台的构建系统生成器,它被广泛用于 C++ 项目。
    • 它可以生成各种构建系统所需的文件,如 Makefile、 Ninja、Visual Studio 项目等。
    • CMake 支持外部库的查找和集成,使得依赖管理变得相对简单。
    • 它可以通过 CPack 生成安装包,方便用户安装和使用你的库。
  2. Conan
    • Conan 是一个 C++ 包管理器,它可以帮助你创建、构建和共享二进制包。
    • 它支持多种版本控制和依赖解析,可以与 CMake、MSBuild、Autotools 等多种构建系统集成。
    • Conan 社区提供了一个中央仓库,你可以将你的库上传到那里,供其他开发者使用。
  3. vcpkg
    • vcpkg 是一个 C++ 库管理器,用于 Windows、Linux 和 macOS。
    • 它提供了一个命令行工具,用于安装 C++ 库并集成到你的项目中。
    • 你可以将你的库添加到 vcpkg 的仓库中,这样用户就可以通过 vcpkg 安装你的库。
  4. Traditional Packaging
    • 你也可以选择传统的打包方式,比如为不同的操作系统创建 Debian 包、RPM 包、Homebrew Formula 等。
    • 这通常涉及到更多的手动工作,但可以为用户提供熟悉的安装体验。 选择哪种工具取决于多个因素,包括你的库的复杂性、目标用户群体、构建和分发流程的自动化需求等。如果你的库是作为一个更大的项目的一部分,那么你可能希望选择与该项目兼容的工具。如果你的目标是广泛的 C++ 社区,那么 Conan 或 vcpkg 可能是不错的选择,因为它们提供了简单的集成和广泛的库支持。如果你需要更细粒度的控制和对构建流程的优化,Bazel 或 CMake 可能更适合你的需求。

相关工作

在参考5中,作者讲了基本的发布程序的逻辑,也就是打包,分发。我这里需要补充一点,分发程序分为源码分发自行编译,以及二进制分发。前者安全可控,后者省时省力但不一定安全。

总结

bazel是一个用于cpp工程构建的工具,但是其学习成本高昂,曲线陡峭,一般人只需要掌握基本用法就行了。

不建议在这上面花太多时间浪费生命。

参考

1 Installing Bazel on macOS bazel.build/install/os-…

2 bazel 是一把双刃剑 www.jianshu.com/p/ab5ef02bf…

3 bazel official: Bazel Tutorial: Build a C++ Project, bazel.build/start/cpp

4 Bazel & CMake | HJiahu's Blog (yearn.xyz)

5 基于google Bazel 编译和打包springboot项目 - 掘金 (juejin.cn)

6 C++程序如何发布? |21xrx.com