About Bazel
参考文档:
1. Bazel介绍
Bazel 是一个开源的构建和测试工具,由 Google 开发,主要用于管理大型项目的构建过程。
1.1 Bazel构建工作流程
当我们使用Bazel来构建的时候,其工作流程大致如下:
-
Load: 加载target相关的BUILD文件
-
Analyze: Bazel通过分析所有BUILD文件,使用指定的构建规则,生成一个表示依赖关系的有向无环图(DAG)
-
Execute: 针对target的输入执行相应动作产生输出。
1.2 Bazel的使用
参看:
使用Bazel来构建工程时,我们通常按如下步骤:
-
安装Bazel
下载并安装Bazel.
-
创建工程workspace
workspace是bazel用于查找Inputs及BUILD文件的目录,构建时的输出也会保存到该牡蛎
-
编写BUILD文件
BUILD文件用于告诉Bazel如何进行构建。在BUILD文件中声明了
build targets
,构建时Bazel读取该文件,按照对应的build rule
完成构建目标的输出。BUILD文件采用Starlark语言进行编写,后面会有介绍。
-
build target指明了构建时的输入(inputs)、依赖(dependencies)、规则(build rule)以及规则的相关配置选项。
-
build rule指明了Bazel构建时所使用的工具(比如:编译器和链接器)及工具的相关配置。Bazel内置了一系列的build rules,覆盖了所支持目标平台编程语言的大部分构建类型。 我们可以从这里找到这些内置的构建规则,当然也可以使用Starlark来扩展build rule。
-
-
运行Bazel
从命令行Bazel完成构建,对应的构建输出也会在工程目录内。
2. 安装Bazel
参看:
我们有两种方式来安装Bazel:可以使用官方推荐的Bazelisk来安装,也可以到Release下载对应的Bazel二进制文件来安装。
2.1 直接下载二进制安装
我们可以到Bazel Releases页面下载。这里针对我们的Ubuntu操作系统下载如下文件即可:
# wget https://github.com/bazelbuild/bazel/releases/download/8.0.0rc6/bazel-8.0.0rc6-installer-linux-x86_64.sh # chmod 777 ./bazel-8.0.0rc6-installer-linux-x86_64.sh # ./bazel-8.0.0rc6-installer-linux-x86_64.sh
2.2 采用bazelisk安装bazel
bazelisk采用golang编写,是对bazel的一个包装。它可以自动的为当前工作目录选择一个合适的Bazel版本,然后有必要的话就从官方网站上进行下载,并将传递给bazelisk的命令转发到bazel上。
bazelisk官网: https://github.com/bazelbuild/bazelisk
这里我们简单记录一下bazelisk的安装过程。
2.2.1 下载bazelisk
到bazelisk release页面下载bazelisk安装包。这里我们下载linux-amd64
版本:
# uname -p
x86_64
# cd /usr/local/
# mkdir bazel && cd bazel
# wget https://github.com/bazelbuild/bazelisk/releases/download/v1.24.1/bazelisk-linux-amd64
# chmod 777 ./bazelisk-linux-amd64
2.2.2 为bazelisk-linux-amd64创建软连接
因为我们使用的使用都是直接输入bazel
或bazelisk
命令,因此我们在这里为步骤1)下载的二进制文件创建两个软连接:
# pwd
/usr/local/bazel
# ln -sf ./bazelisk-linux-amd64 bazel
# ln -sf ./bazelisk-linux-amd64 bazelisk
2.2.3 将bazelisk安装目录添加到环境变量
修改`~/.bashrc`文件,将bazelisk安装目录添加到环境变量:
```
export BAZELROOT=/usr/local/bazel
export PATH=$PATH:$BAZELROOT
```
记得修改完后执行`source ~/.bashrc`使设置生效。
2.2.4 指定bazel版本
由于下载下来的bazelisk只是bazel的一个包装壳,我们直接执行bazelisk-linux-amd64
的话可能会卡很长时间。下面我们来介绍一下bazelisk选择Bazel版本的内部算法:
-
如果指定了
USE_BAZEL_VERSION
环境变量,那么bazelisk会使用该环境变量所指定的bazel版本 -
否则,假如当前workspace的根目录下存在
.bazeliskrc
文件并且在其中通过USE_BAZEL_VERSION
变量指定了bazel版本的话,那么就使用该版本的bazel -
否则,假如当前目录或者递归的父目录中存在
.bazelversion
,bazelisk就会尝试读取该文件并使用其指定版本的bazel -
否则,假如环境变量
USE_BAZEL_FALLBACK_VERSION
指定了的话,就使用该环境变量所设置的bazel。设置格式如下:-
假如所设置的值以
error:
开头,则在使用bazelisk时会报告一个错误,并且不去探测所设定版本的bazel -
假如所设置的值以
warn:
开头,那么在使用bazelisk时就会产生一个警告信息,并且使用其所指定版本的bazel -
假如所设置的值以
silent:
开头,那么bazelisk就会使用其所指定版本的bazel
-
-
否则,bazelisk会使用官方最新的Bazel版本
根据上面的描述,如果我们有workspace的话我们倾向于通过.bazeliskrc
来指定;如果作为学习,在命令行中使用的话,我们倾向于通过USE_BAZEL_FALLBACK_VERSION
环境变量来指定。这里我们修改~/.bashrc
文件,添加如下内容:
export USE_BAZEL_FALLBACK_VERSION=warn:8.0.0rc6
执行如下命令使环境变量生效:
# source ~/.bashrc
2.2.5 执行bazel(或bazelisk)命令,查看版本号
到此步骤我们就可以执行如下命令查看bazel的版本号了:
# bazel --version
2024/12/04 00:27:40 Warning: used fallback version "8.0.0rc6"
2024/12/04 00:27:40 Downloading https://releases.bazel.build/8.0.0/rc6/bazel-8.0.0rc6-linux-x86_64...
Downloading: 61 MB out of 61 MB (100%)
bazel 8.0.0rc6
# ls
bazel bazelisk bazelisk-linux-amd64
通过上面的执行过程,我们看到第一次执行的时候bazelisk会从类似https://github.com/<FORK>/bazel/releases/download/<VERSION>/<FILENAME>
的位置去下载指定版本的bazel。
2.2.6 备注说明
经过前面的步骤,安装过程就完成了,但是还有个点我们简单介绍一下:bazelisk下载下载下来的指定版本的bazel放在什么位置呢?其实在官网中也有描述:
It creates a directory called "bazelisk" inside your [user cache directory]
(https://golang.org/pkg/os/#UserCacheDir) and will store them there. Feel
free to delete this directory at any time, as it can be regenerated
automatically when required.
这里在Linux操作系统下的cache目录为~/.cache
:
# ls ~/.cache
bazelisk Microsoft motd.legal-displayed vscode-cpptools
# tree ~/.cache/bazelisk/
/root/.cache/bazelisk/
└── downloads
├── metadata
│ └── bazelbuild
│ └── bazel-8.0.0rc6-linux-x86_64
├── sha256
│ └── db6de55999ddbbf28215eddfb19b7d0f688a42945193887048183bd5d3e991d9
│ └── bin
│ └── bazel
└── _tmp
7 directories, 2 files
3. Bazel工程示例
可以直接从Github上克隆相应的示例:
# git clone https://github.com/bazelbuild/examples
4. 编写BUILD文件
这里我们先来看看Bazel工程示例中两个典型的example:
-
examples/cpp-tutorial/stage1
-
examples/go-tutorial/stage1
-
cpp: stage1示例
该工程的目录结构如下:
. |-- main | |---- BUILD | |---- hello-world.cc |-- MODULE.bazel |-- README.md
这里我们来看看main/BUILD的内容:
cc_binary( name = "hello-world", srcs = ["hello-world.cc"], )
-
go: stage1示例
该工程的目录结构如下:
. |-- .bazelversion |-- BUILD |-- MODULE.bazel |-- hello.go
其中:
-
.bazelversion文件用于指明所使用的Bazel版本
-
BUILD文件用于指明所构建的目标及规则
-
MODULE.bazel用于管理项目模块化和依赖关系的关键文件
这里我们主要来看看BUILD文件的相关内容:
load("@rules_go//go:def.bzl", "go_binary") go_binary( name = "hello", srcs = ["hello.go"], )
-
4.1 StarLark语言
Bazel的BUILD文件是采用StarLark语言进行编写的,这里我们先简单介绍该语言。
参看:
StarLark是Python的一种方言(dialect),主要用作配置语言。StarLark解释器通常是会被嵌入到另一个更大型的应用程序中,除了使用其基本语言核心功能之外,还可能会在应用程序中定义一些额外的领域特定函数和数据类型。比如,StarLark就被嵌入到了Bazel构建工具当中。
我们可以搭建独立的StarLark运行环境,也可以直接使用Bazel来运行。
4.1.1 搭建独立的StarLark运行环境
-
安装golang环境
略
-
安装StarLark CLI
# go install go.starlark.net/cmd/starlark@latest
-
编写StarLark脚本
示例: src/main.star
print("hello, world")
-
运行脚本
# starlark src/main.star hello, world
4.1.2 使用Bazel来运行Starlark
-
创建Starlark脚本并定义构建规则
-
示例: src/main.bzl
def _hello_world_impl(ctx): output = ctx.actions.declare_file("hello.txt") ctx.actions.write( output = output, content = "Hello from Starlark!", ) return [DefaultInfo(files = depset([output]))] hello_world = rule( implementation = _hello_world_impl, )
-
定义构建目标:BUILD
load("//:src/main.bzl", "hello_world") hello_world( name = "generate_hello", visibility = ["//visibility:public"], )
-
创建MODULE.bazel
这里直接创建一个空文件即可。
-
-
构建与运行
通过 Bazel 执行构建和测试。
# bazel build //:generate_hello
4.2 Bazel内置rule
在我们前面的examples/cpp-tutorial/stage1示例中,有如下规则:
cc_binary(
name = "hello-world",
srcs = ["hello-world.cc"],
)
这是一个Bazel工具内置的规则。关于Bazel的内置规则,我们可以从如下地方找到相关资料:
下面我们介绍几个内置的Bazel rule:
-
cc_binary规则
该规则规则用于产生一个可执行的二进制文件,实现代码在:https://github.com/bazelbuild/bazel/tree/master/src/main/starlark/builtins_bzl/common/cc/cc_binary.bzl
通过源代码我们可以看到
cc_binary
支持如下参数:cc_binary = rule( attrs = cc_binary_attrs, )
4.2.1 如何修改cc_binary的toolchains来调整链接顺序
如下是来自deepseek关于如何修改bazel cc_binary的toolchains来调整链接顺序
的回答。
在 Bazel 中调整 cc_binary 的链接顺序,通常需要修改 C++ 工具链(C++ Toolchain)的配置,尤其是链接器(Linker)的参数。以下是分步实现方法:
-
理解 Bazel 的 C++ 工具链
Bazel 的 C++ 工具链通过 cc_toolchain_config 规则定义,其核心配置文件会指定编译、链接等操作的命令行参数。链接顺序通常由以下因素决定:
-
deps 的顺序:cc_binary 的 deps 中库的顺序会影响链接顺序(后依赖的库优先)。
-
工具链的链接参数:工具链中定义的链接器标志(如 linker_flag)可以强制调整顺序。
若需要全局调整链接顺序(而非逐个目标修改 deps),需自定义工具链。
-
-
自定义 C++ 工具链
-
步骤 1:创建工具链配置文件
在项目目录(如 //toolchains/cpp)中创建工具链定义文件 BUILD.bazel 和 cc_toolchain_config.bzl。
# toolchains/cpp/BUILD.bazel load(":cc_toolchain_config.bzl", "cc_toolchain_config") cc_toolchain_config( name = "my_cc_toolchain_config", cpu = "k8", # 根据目标平台调整,如 x86_64、arm64 ) filegroup(name = "empty") cc_toolchain( name = "my_cc_toolchain", toolchain_config = ":my_cc_toolchain_config", all_files = ":empty", ar_files = ":empty", as_files = ":empty", compiler_files = ":empty", dwp_files = ":empty", linker_files = ":empty", objcopy_files = ":empty", strip_files = ":empty", ) toolchain( name = "my_cc_toolchain_impl", exec_compatible_with = ["@platforms//os:linux"], target_compatible_with = ["@platforms//os:linux"], toolchain = ":my_cc_toolchain", toolchain_type = "@bazel_tools//tools/cpp:toolchain_type", )
-
步骤 2:定义链接参数
在 cc_toolchain_config.bzl 中调整链接顺序。例如,强制将某些库放在前面或后面:
# toolchains/cpp/cc_toolchain_config.bzl load("@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl", "feature", "flag_group", "flag_set") def _impl(ctx): # 基础工具链配置(参考 Bazel 官方模板) features = [ feature( name = "default_linker_flags", enabled = True, flag_sets = [ flag_set( actions = ["all_link_actions"], flag_groups = [ flag_group( flags = [ # 调整链接顺序的示例: # 1. 强制链接 libA 在 libB 之前 "-Wl,--start-group", "-lA", "-lB", "-Wl,--end-group", # 2. 添加全局库路径 "-L/path/to/custom/libs", ], ), ], ), ], ), ] # 返回工具链配置 return cc_common.create_cc_toolchain_config_info( ctx = ctx, features = features, cxx_builtin_include_directories = [ "/usr/include", "/usr/local/include", ], toolchain_identifier = "my-custom-toolchain", host_system_name = "local", target_system_name = "local", target_cpu = "k8", target_libc = "glibc_2.31", compiler = "gcc", abi_version = "gcc", abi_libc_version = "glibc_2.31", ) cc_toolchain_config = rule( implementation = _impl, attrs = {}, provides = [CcToolchainConfigInfo], )
-
-
注册自定义工具链
在 WORKSPACE 或 WORKSPACE.bzlmod 中注册工具链:
# WORKSPACE register_toolchains("//toolchains/cpp:my_cc_toolchain_impl")
或在命令行中指定:
# bazel build --extra_toolchains=//toolchains/cpp:my_cc_toolchain_impl ...
-
在 cc_binary 中使用自定义工具链
在 cc_binary 中显式指定工具链(可选):
cc_binary( name = "my_binary", srcs = ["main.cpp"], deps = ["//mylib"], toolchains = ["//toolchains/cpp:my_cc_toolchain_impl"], )
-
高级调整:通过 –linkopt 或 features
-
方法 1:通过
--linkopt
传递参数在命令行中直接覆盖链接参数:
bazel build //:my_binary --linkopt="-Wl,--start-group" --linkopt="-lA" --linkopt="-lB" --linkopt="-Wl,--end-group"
-
方法 2:使用 features 动态控制
在工具链配置中定义可开关的 feature:
# cc_toolchain_config.bzl feature( name = "custom_link_order", enabled = False, flag_sets = [ flag_set( actions = ["all_link_actions"], flag_groups = [ flag_group(flags = ["-Wl,--start-group", "-lA", "-lB", "-Wl,--end-group"]), ], ), ], )
在 cc_binary 中启用该特性:
cc_binary( name = "my_binary", srcs = ["main.cpp"], features = ["custom_link_order"], )
-
-
验证链接顺序
检查生成的链接命令:
# bazel build --subcommands //:my_binary
输出中应包含类似以下内容:
SUBCOMMAND: linking my_binary ... /usr/bin/gcc ... -Wl,--start-group -lA -lB -Wl,--end-group ...
关键注意事项:
-
平台兼容性:
-
不同平台(Linux/macOS/Windows)的链接器(ld/lld/link)参数不同,需针对性调整。
-
例如 macOS 使用 -force_load 或 -all_load 替代 –start-group。
-
-
工具链覆盖范围:
- 自定义工具链会影响所有使用该工具链的目标,确保全局调整是必要的。
-
依赖顺序:
- 即使调整工具链,deps 中的顺序仍可能影响链接结果,建议同时检查 deps 顺序。
-
工具链继承
-
可基于 Bazel 默认工具链扩展,避免重复配置。例如:
load("@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl", "feature", "flag_group", "flag_set")
-
-
4.3 Bazel扩展rule
参看:
4.4 高级: bazel中rule函数的实现
通过上面Bazel扩展rule
一节,我们发现要实现一个自己的简单扩展rule并不复杂。本身rule是bazel的一个内置函数。这里我们来看看该内置函数的实现,以进一步理解bazel。
如下是来自于deepseek的关于bazel rule函数的实现源代码位置
的回答。
4.4.1 Bazel的rule()函数
Bazel 的 rule() 函数是 Starlark 语言中用于自定义构建规则的核心接口,其底层实现主要位于 Bazel 的 Java 源代码中。以下是其实现的关键代码位置及功能解析:
-
Starlark 层的 rule() 函数定义
-
代码位置:Bazel 的 Starlark 内置函数 rule() 通过 Java 代码暴露给 Starlark 环境。其入口点位于 NativeModule 类中,负责将 Java 实现的函数绑定到 Starlark 的全局命名空间。
-
相关文件:src/main/java/com/google/devtools/build/lib/starlarkbuildapi/StarlarkRuleFunctionsApi.java
-
功能:定义 rule() 函数的参数(如 implementation、attrs 等),并验证参数合法性
-
-
-
规则类的构建与注册
-
RuleClass 类:规则的核心元数据(属性、输出模板、实现函数等)通过 RuleClass 类存储
-
代码位置:src/main/java/com/google/devtools/build/lib/packages/RuleClass.java
-
关键逻辑
-
解析 attrs 参数,生成属性类型和约束条件。
-
注册规则的输出文件模板(如 outs)。
-
处理规则的隐式依赖和配置过渡(configuration transitions)
-
-
-
RuleFactory 类:负责将 Starlark 层的规则定义转换为 Java 层的 RuleClass 对象
-
代码位置:src/main/java/com/google/devtools/build/lib/packages/RuleFactory.java
-
功能:验证规则属性、处理继承关系(如 parent 规则)并生成最终的 RuleClass
-
-
-
规则实现函数的执行
-
StarlarkRuleImplementation 类:处理规则的分析阶段逻辑,即调用用户定义的 implementation 函数
-
代码位置:src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkRuleImplementation.java
-
关键流程:
-
创建 ctx 对象(StarlarkRuleContext),提供属性访问、动作声明等接口。
-
执行用户定义的 implementation 函数,生成动作(Action)和提供程序(Provider)
-
-
-
-
动作(Action)的生成与管理
-
ctx.actions 的实现:通过 StarlarkActionFactory 类将 Starlark 中的动作声明(如 run(), write())转换为 Bazel 内部的 Action 对象。
-
代码位置:src/main/java/com/google/devtools/build/lib/analysis/starlark/StarlarkActionFactory.java 713。
-
关联类:ActionOwner(动作所有者)、CommandLine(命令行参数生成)等
-
-
-
底层依赖与工具链集成
-
工具链解析:规则可能依赖工具链(如 C++ 编译器、Sass 编译器),相关逻辑位于 ToolchainResolutionFunction
- 代码位置:src/main/java/com/google/devtools/build/lib/analysis/ToolchainResolutionFunction.java
-
平台与配置过渡:处理跨平台构建和配置参数传递,涉及 Transition 接口和 ConfigurationResolver 类
-
4.4.2 如何用java扩展starlark
如下是来自于deepseek关于如何用java扩展starlark
的回答:
在 Bazel 中,Starlark(一种 Python 方言)的运行环境是由 Java 实现的,因此通过 Java 扩展 Starlark 的核心方法是定义新的 Starlark 内置函数或类型,并将其暴露给 Starlark 脚本使用。以下是具体实现步骤和示例。
-
扩展 Starlark 的基本流程
要实现 Java 对 Starlark 的扩展,需要:
-
定义 Java 类:实现函数逻辑或自定义数据类型。
-
暴露给 Starlark:通过注解或注册机制,将 Java 类的方法绑定到 Starlark 环境。
-
集成到 Bazel:将扩展代码编译到 Bazel 或插件中,供 Starlark 脚本调用
-
-
扩展 Starlark 函数(示例)
假设我们要在 Starlark 中添加一个 hello(name) 函数,打印问候语。
-
步骤 1:定义 Java 类
// 文件路径: src/main/java/com/example/starlark/HelloModule.java package com.example.starlark; import net.starlark.java.annot.Param; import net.starlark.java.annot.StarlarkMethod; import net.starlark.java.eval.StarlarkValue; import net.starlark.java.eval.Starlark; public class HelloModule implements StarlarkValue { // 定义 Starlark 方法 @StarlarkMethod( name = "hello", documented = false, parameters = { @Param(name = "name", type = String.class, named = true) } ) public String hello(String name) { return "Hello, " + name + "!"; } }
-
步骤 2:注册到 Starlark 环境
在 Bazel 的全局模块中注册自定义模块:
// 文件路径: src/main/java/com/example/starlark/CustomStarlarkModule.java package com.example.starlark; import com.google.devtools.build.lib.starlarkbuildapi.Core; public class CustomStarlarkModule implements Core { @Override public void addBindingsToBuilder(ImmutableMap.Builder<String, Object> builder) { builder.put("hello", new HelloModule()); } }
-
步骤 3:在 Bazel 中加载扩展
在 Bazel 的初始化流程中加载自定义模块:
// 修改 Bazel 的初始化代码(通常需要重新编译 Bazel) public class BazelMain { public static void main(String[] args) { // 注册自定义模块 Starlark.addModule(new CustomStarlarkModule()); // 启动 Bazel ... } }
-
-
扩展 Starlark 数据类型
若需要定义新的 Starlark 类型(如自定义对象),可以继承 StarlarkValue:
-
示例:定义 User 类型
// 文件路径: src/main/java/com/example/starlark/User.java @StarlarkBuiltin(name = "User", doc = "Represents a user object.") public class User implements StarlarkValue { private final String name; public User(String name) { this.name = name; } @StarlarkMethod(name = "name", doc = "Returns the user's name.") public String getName() { return name; } }
-
在 Starlark 中创建对象
通过工厂方法暴露给 Starlark:
@StarlarkMethod(name = "create_user", parameters = {@Param(name = "name", type = String.class)}) public User createUser(String name) { return new User(name); }
-
-
在 Starlark 脚本中使用扩展
编译并集成后,可以在 .bzl 文件中直接调用:
# 调用自定义函数 greeting = hello("World") # 返回 "Hello, World!" # 使用自定义类型 user = create_user("Alice") print(user.name) # 输出 "Alice"
-
关键技术点
-
注解驱动:
-
@StarlarkMethod:标记方法或类型,定义 Starlark 可见性。
-
@Param:定义函数参数类型和约束。
-
@StarlarkBuiltin:定义自定义类型的元数据
-
-
类型映射:
-
Java 类型(如 String、List)会自动映射到 Starlark 类型。
-
复杂类型需实现 StarlarkValue 接口。
-
-
线程安全:
Starlark 是线程安全的,确保扩展代码无副作用
-
集成到 Bazel:
- 若扩展是为 Bazel 定制,需将代码编译到 Bazel 的 @_builtins 或通过插件机制加载。
-
-
调试与验证
-
单元测试:使用 Starlark 的 Java 测试框架
@Test public void testHelloFunction() throws Exception { StarlarkThread thread = new StarlarkThread(); Object result = Starlark.call(thread, new HelloModule(), "hello", ImmutableList.of("Alice")); assertThat(result).isEqualTo("Hello, Alice!"); }
-
Bazel 加载:通过 –experimental_builtins_bzl_path 参数加载自定义模块。
-
-
实际应用场景
-
自定义构建规则:封装复杂构建逻辑(如代码生成、工具链调用)。
-
工具链集成:将外部工具(如代码检查器)暴露为 Starlark 函数。
-
元编程:动态生成 BUILD 文件内容
-
5. 运行Bazel
参看:
从命令行中运行Bazel很简单,基本语法格式如下:
# bazel [<startup options>] <command> [<args>] -- [<target patterns>]
举例:
# bazel build //main:hello-world