目录
安装
Ubuntu
参考下面的步骤安装Bazel:
echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add - sudo apt-get update && sudo apt-get install bazel
可以用如下命令升级到最新版本的Bazel:
sudo apt-get install --only-upgrade bazel
Bazelisk
这是基于Go语言编写的Bazel启动器,它会为你的工作区下载最适合的Bazel,并且透明的将命令转发给该Bazel。
由于Bazellisk提供了和Bazel一样的接口,因此通常直接将其命名为bazel:
sudo wget -O /usr/local/bin/bazel https://github.com/bazelbuild/bazelisk/releases/download/v0.0.8/bazelisk-linux-amd64 sudo chmod +x /usr/local/bin/bazel
个人更加推荐后面这种。
概念和术语
Workspace
工作空间是一个目录,它包含:
- 构建目标所需要的源码文件,以及相应的BUILD文件
- 指向构建结果的符号链接
- WORKSPACE文件,可以为空,可以包含对外部依赖的引用
下面是一个样例:
. ├── BUILD ├── main.cc ├── MODULE.bazel ├── README.md ├── WORKSPACE └── WORKSPACE.bzlmod
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( name = "com_github_google_glog", sha256 = "eede71f28371bf39aa69b45de23b329d37214016e2055269b3b5e7cfd40b59f5", strip_prefix = "glog-0.5.0", urls = ["https://github.com/google/glog/archive/refs/tags/v0.5.0.tar.gz"], ) # We have to define gflags as well because it's a dependency of glog. http_archive( name = "com_github_gflags_gflags", sha256 = "34af2f15cf7367513b352bdcd2493ab14ce43692d2dcd9dfc499492966c64dcf", strip_prefix = "gflags-2.2.2", urls = ["https://github.com/gflags/gflags/archive/refs/tags/v2.2.2.tar.gz"], )
Package
包是工作空间中主要的代码组织单元,其中包含一系列相关的文件(主要是代码)以及描述这些文件之间关系的BUILD文件
包是工作空间的子目录,它的根目录必须包含文件BUILD.bazel或BUILD。除了那些具有BUILD文件的子目录——子包——以外,其它子目录属于包的一部分
├── lib │ ├── BUILD │ ├── hello-time.cc │ └── hello-time.h ├── main │ ├── BUILD │ ├── hello-greet.cc │ ├── hello-greet.h │ └── hello-world.cc ├── README.md └── WORKSPACE
Target
包是一个容器,它的元素定义在BUILD文件中,包括:
- 规则(Rule),指定输入集和输出集之间的关系,声明从输入产生输出的必要步骤。一个规则的输出可以是另外一个规则的输入
- 文件(File),可以分为两类:
-
- 源文件
- 自动生成的文件(Derived files),由构建工具依据规则生成
- 包组:一组包,包组用于限制特定规则的可见性。包组由函数package_group定义,参数是包的列表和包组名称。你可以在规则的visibility属性中引用包组,声明那些包组可以引用当前包中的规则
任何包生成的文件都属于当前包,不能为其它包生成文件。但是可以从其它包中读取输入
Label
引用一个目标时需要使用“标签”。标签的规范化表示: @project//my/app/main:app_binary, 冒号前面是所属的包名,后面是目标名。如果不指定目标名,则默认以包路径最后一段作为目标名,例如:
//my/app //my/app:app
这两者是等价的。在BUILD文件中,引用当前包中目标时,包名部分可以省略,因此下面四种写法都可以等价:
# 当前包为my/app //my/app:app //my/app :app app
在BUILD文件中,引用当前包中定义的规则时,冒号不能省略。引用当前包中文件时,冒号可以省略。 例如: generate.cc。
但是,从其它包引用时、从命令行引用时,都必须使用完整的标签: //my/app:generate.cc
@project这一部分通常不需要使用,引用外部存储库中的目标时,project填写外部存储库的名字。
Rule
规则指定输入和输出之间的关系,并且说明产生输出的步骤。
规则有很多类型。每个规则都具有一个名称属性,此名称亦即目标名称。对于某些规则,此名称就是产生的输出的文件名。
在BUILD中声明规则的语法时:
规则类型( name = "...", 其它属性 = ... )
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") load("//tools/install:install.bzl", "install", "install_src_files") load("//tools:cpplint.bzl", "cpplint") package(default_visibility = ["//visibility:public"]) MONITOR_COPTS = ['-DMODULE_NAME=\\"monitor\\"'] cc_binary( name = "libmonitor.so", linkshared = True, linkstatic = True, deps = [ ":monitor_lib", ], ) cc_library( name = "monitor_lib", srcs = ["monitor.cc"], hdrs = ["monitor.h"], copts = MONITOR_COPTS, visibility = ["//visibility:private"], deps = [ "//cyber", "//modules/common/util:util_tool", "//modules/monitor/common:recurrent_runner", "//modules/monitor/hardware:esdcan_monitor", "//modules/monitor/hardware:gps_monitor", "//modules/monitor/hardware:resource_monitor", "//modules/monitor/hardware:socket_can_monitor", "//modules/monitor/software:camera_monitor", "//modules/monitor/software:channel_monitor", "//modules/monitor/software:functional_safety_monitor", "//modules/monitor/software:latency_monitor", "//modules/monitor/software:localization_monitor", "//modules/monitor/software:module_monitor", "//modules/monitor/software:process_monitor", "//modules/monitor/software:recorder_monitor", "//modules/monitor/software:summary_monitor", ], alwayslink = True, ) filegroup( name = "runtime_data", srcs = glob([ "dag/*.dag", "launch/*.launch", ]), ) install( name = "install", library_dest = "monitor/lib", data_dest = "monitor", targets = [ ":libmonitor.so", ], data = [ ":runtime_data", ":cyberfile.xml", ":monitor.BUILD", ], ) install_src_files( name = "install_src", deps = [ ":install_monitor_src", ":install_monitor_hdrs" ], ) install_src_files( name = "install_monitor_src", src_dir = ["."], dest = "monitor/src", filter = "*", ) install_src_files( name = "install_monitor_hdrs", src_dir = ["."], dest = "monitor/include", filter = "*.h", ) cpplint()
BUILD文件
BUILD文件定义了包的所有元数据。其中的语句被从上而下的逐条解释,某些语句的顺序很重要, 例如变量必须先定义后使用,但是规则声明的顺序无所谓。
BUILD文件仅能包含ASCII字符,且不得声明函数、使用for/if语句,你可以在Bazel扩展——扩展名为.bzl的文件中声明函数、控制结构。并在BUILD文件中用load语句加载Bazel扩展:
load("//foo/bar:file.bzl", "some_library")
上面的语句加载foo/bar/file.bzl并添加其中定义的符号some_libraray到当前环境中,load语句可以用来加载规则、函数、常量(字符串、列表等)。
load语句必须出现在顶级作用域,不能出现在函数中。第一个参数说明扩展的位置,你可以为导入的符号设置别名。
规则的类型,一般以编程语言为前缀,例如cc,java,后缀通常有:
- *_binary 用于构建目标语言的可执行文件
- *_test 用于自动化测试,其目标是可执行文件,如果测试通过应该退出0
- *_library 用于构建目标语言的库
Dependency
目标A依赖B,就意味着A在构建或执行期间需要B。所有目标的依赖关系构成非环有向图(DAG)称为依赖图。
距离为1的依赖称为直接依赖,大于1的依赖则称为传递性依赖。
依赖分为以下几种:
- srcs依赖:直接被当前规则消费的文件
- deps依赖:独立编译的模块,为当前规则提供头文件、符号、库、数据
- data依赖:不属于源码,不影响目标如何构建,但是目标在运行时可能依赖之
bzl文件
全局的变量需要存放的位置
module( name = "example", version = "0.0.1", ) # 1. The metadata of glog is fetched from the BCR, including its dependencies (gflags). # 2. The `repo_name` attribute allows users to reference this dependency via the `com_github_google_glog` repo name. bazel_dep(name = "glog", version = "0.5.0", repo_name = "com_github_google_glog")
构建C++ 项目
通过Bazel构建
第一步是创建工作空间。工作空间中包含以下特殊文件:
- WORKSPACE,此文件位于根目录中,将当前目录定义为Bazel工作空间
- BUILD,告诉Bazel项目的不同部分如何构建。工作空间中包含BUILD文件的目录称为包
当Bazel构建项目时,所有的输入和依赖都必须位于工作空间中。除非被链接,不同工作空间的文件相互独立没有关系。
每个BUILD文件包含若干Bazel指令,其中最重要的指令类型是构建规则(Build Rule),构建规则说明如何产生期望的输出——例如可执行文件或库。 BUILD中的每个构建规则也称为目标(Target),目标指向若干源文件和依赖,也可以指向其它目标。
cc_binary( name = "hello-world", srcs = ["hello-world.cc"], )
这里定义了一个名为hello-world的目标,它使用了内置的cc_binary规则。该规则告诉Bazel,从源码hello-world.cc构建一个自包含的可执行文件。
执行下面的命令可以触发构建:
# //main: BUILD文件相对于工作空间的位置 # hello-world 是BUILD文件中定义的目标 bazel build //main:hello-world
构建完成后,工作空间根目录会出现bazel-bin等目录,它们都是指向$HOME/.cache/bazel某个后代目录的符号链接。执行:
bazel-bin/main/hello-world
可以运行构建好的二进制文件。
查看依赖图
Bazel会根据BUILD中的声明产生一张依赖图,并根据这个依赖图实现精确的增量构建。
要查看依赖图,先安装:
sudo apt install graphviz xdot
然后执行:
bazel query --nohost_deps --noimplicit_deps 'deps(//main:hello-world)' --output graph | xdot
指定多个目标
大型项目通常会划分为多个包、多个目标,以实现更快的增量构建、并行构建。工作空间stage2包含单个包、两个目标:
# 首先构建hello-greet库,cc_library是内建规则 cc_library( name = "hello-greet", srcs = ["hello-greet.cc"], # 头文件 hdrs = ["hello-greet.h"], ) # 然后构建hello-world二进制文件 cc_binary( name = "hello-world", srcs = ["hello-world.cc"], deps = [ # 提示Bazel,需要hello-greet才能构建当前目标 # 依赖当前包中的hello-greet目标 ":hello-greet", ], )
使用多个包
工作空间stage3更进一步的划分出新的包,提供打印时间的功能:
cc_library( name = "hello-time", srcs = ["hello-time.cc"], hdrs = ["hello-time.h"], # 让当前目标对于工作空间的main包可见。默认情况下目标仅仅被当前包可见 visibility = ["//main:__pkg__"], )
cc_library( name = "hello-greet", srcs = ["hello-greet.cc"], hdrs = ["hello-greet.h"], ) cc_binary( name = "hello-world", srcs = ["hello-world.cc"], deps = [ # 依赖当前包中的hello-greet目标 ":hello-greet", # 依赖工作空间根目录下的lib包中的hello-time目标 "//lib:hello-time", ], )
如何引用目标
在BUILD文件或者命令行中,你都使用标签(Label)来引用目标,其语法为:
//path/to/package:target-name # 当引用当前包中的其它目标时,可以: //:target-name # 当引用当前BUILD文件中其它目标时,可以: :target-name
Starlark
Starlark支持的数据类型包括:None、bool、dict、function、int、list、string,以及两种Bazel特有的类型:depset、struct。
# 定义一个数字 number = 18 # 定义一个字典 people = { "Alice": 22, "Bob": 40, "Charlie": 55, "Dave": 14, } names = ", ".join(people.keys()) # 定义一个函数 def greet(name): """Return a greeting.""" return "Hello {}!".format(name) # 调用函数 greeting = greet(names) def fizz_buzz(n): """Print Fizz Buzz numbers from 1 to n.""" # 循环结构 for i in range(1, n + 1): s = "" # 分支结构 if i % 3 == 0: s += "Fizz" if i % 5 == 0: s += "Buzz" print(s if s else i)
变量
你可以在BUILD文件中声明和使用变量。使用变量可以减少重复的代码:
COPTS = ["-DVERSION=5"] cc_library( name = "foo", copts = COPTS, srcs = ["foo.cc"], ) cc_library( name = "bar", copts = COPTS, srcs = ["bar.cc"], deps = [":foo"], )
跨BUILD变量
如果要声明跨越多个BUILD文件共享的变量,必须把变量放入.bzl文件中,然后通过load加载bzl文件。
Make变量
所谓Make变量,是一类特殊的、可展开的字符串变量,这种变量类似Shell中变量替换那样的展开。
Bazel提供了:
- 预定义变量,可以在任何规则中使用
- 自定义变量,在规则中定义。仅仅在依赖该规则的那些规则中,可以使用这些变量
仅仅那些标记为Subject to 'Make variable' substitution的规则属性,才可以使用Make变量。例如:
my_attr = "prefix $(FOO) suffix"
如果要使用$字符,需要用 $$代替。
一般预定义变量
执行命令: bazel info --show_make_env [build options]可以查看所有预定义变量的列表。
任何规则都可以使用以下的变量
变量 | 说明 |
COMPILATION_MODE | 编译模式:fastbuild、dbg、opt |
BINDIR | 目标体系结构的二进制树的根目录 |
GENDIR | 目标体系结构的生成代码树的根目录 |
TARGET_CPU | 目标体系结构的CPU |
genrule预定义变量
下表中的变量可以在genrule规则的cmd属性中使用:
变量 | 说明 |
OUTS | genrule的outs列表,如果只有一个输出文件,可以用 $@ |
SRCS | genrule的srcs列表,如果只有一个输入文件,可以用 $< |
@D | 输出目录,如果:
|
输入输出路径变量
下表中的变量以Bazel的Label为参数,获取包的某类输入/输出路径:
一般规则
规则列表
filegroup
为一组目标指定一个名字,你可以从其它规则中方便的引用这组目标。
Bazel鼓励使用filegroup,而不是直接引用目录。Bazel构建系统不能完全了解目录中文件的变化情况,因而文件发生变化时,可能不会进行重新构建。而使用filegroup,即使联用glob,目录中所有文件仍然能够被构建系统正确的监控。
示例:
filegroup( name = "exported_testdata", srcs = glob([ "testdata/*.dat", "testdata/logs/**/*.log", ]), )
要引用filegroup,只需要使用标签:
cc_library( name = "my_library", srcs = ["foo.cc"], data = [ "//my_package:exported_testdata", "//my_package:mygroup", ], )
test_suite
定义一组测试用例,给出一个有意义的名称,便于在特定时机 —— 例如迁入代码、执行压力测试 —— 时执行这些测试用例。
# 匹配当前包中所有small测试 test_suite( name = "small_tests", tags = ["small"], ) # 匹配不包含flaky标记的测试 test_suite( name = "non_flaky_test", tags = ["-flaky"], ) # 指定测试列表 test_suite( name = "smoke_tests", tests = [ "system_unittest", "public_api_unittest", ], )
alias
为规则设置一个别名:
filegroup( name = "data", srcs = ["data.txt"], ) # 定义别名 alias( name = "other", actual = ":data", )
config_setting
通过匹配以Bazel标记或平台约束来表达的“配置状态”,config_setting能够触发可配置的属性。
config_setting( name = "arm_build", values = {"cpu": "arm"}, )
下面的库,通过select来声明可配置属性:
cc_binary( name = "mybinary", srcs = ["main.cc"], deps = select({ # 如果config_settings arm_build匹配正在进行的构建,则依赖arm_lib这个目标 ":arm_build": [":arm_lib"], # 如果config_settings x86_debug_build匹配正在进行的构建,则依赖x86_devdbg_lib ":x86_debug_build": [":x86_devdbg_lib"], # 默认情况下,依赖generic_lib "//conditions:default": [":generic_lib"], }), )
下面的例子,匹配任何定义了宏FOO=bar的针对X86平台的调试(-c dbg)构建:
config_setting( name = "x86_debug_build", values = { "cpu": "x86", "compilation_mode": "dbg", "define": "FOO=bar" }, )
genrule
一般性的规则 —— 使用用户指定的Bash命令,生成一个或多个文件。使用genrule理论上可以实现任何构建行为,例如压缩JavaScript代码。但是在执行C++、Java等构建任务时,最好使用相应的专用规则,更加简单。
不要使用genrule来运行测试,如果需要一般性的测试规则,可以考虑使用sh_test。
genrule在一个Bash shell环境下执行,当任意一个命令或管道失败(set -e -o pipefail),整个规则就失败。你不应该在genrule中访问网络。
genrule( name = "foo", # 不需要输入 srcs = [], # 生成一个foo.h outs = ["foo.h"], # 运行当前规则所在包下的一个Perl脚本 cmd = "./$(location create_foo.pl) > \"$@\"", tools = ["create_foo.pl"], )
C++规则
规则列表
cc_binary
隐含输出:
- name.stripped,仅仅当显式要求才会构建此输出,针对生成的二进制文件运行strip -g以驱除debug符号。额外的strip选项可以通过命令行--stripopt=-foo传入
- name.dwp,仅仅当显式要求才会构建此输出,如果启用了 Fission ,则此文件包含用于远程调试的调试信息,否则是空文件
属性 | 说明 |
name | 目标的名称 |
deps | 需要链接到此二进制目标的其它库的列表,以Label引用 这些库可以是cc_library或objc_library定义的目标 |
srcs | C/C++源文件列表,以Label引用 这些文件是C/C++源码文件或头文件,可以是自动生成的或人工编写的。 所有cc/c/cpp文件都会被编译。如果某个声明的文件在其它规则的outs列表中,则当前规则自动依赖于那个规则 所有.h文件都不会被编译,仅仅供源码文件包含之。所有.h/.cc等文件都可以包含srcs中声明的、deps中声明的目标的hdrs中声明的头文件。也就是说,任何#include的文件要么在此属性中声明,要么在依赖的cc_library的hdrs属性中声明 如果某个规则的名称出现在srcs列表中,则当前规则自动依赖于那个规则:
允许的文件类型:
|
copts | 字符串列表 为C++编译器提供的选项,在编译目标之前,这些选项按顺序添加到COPTS。这些选项仅仅影响当前目标的编译,而不影响其依赖。选项中的任何路径都相对于当前工作空间而非当前包 也可以在bazel build时通过--copts选项传入,例如: Shell --copt"-DENVOY_IGNORE_GLIBCXX_USE_CXX11_ABI_ERROR=1" |
defines | 字符串列表 为C++编译器传递宏定义,实际上会前缀以-D并添加到COPTS。与copts属性不同,这些宏定义会添加到当前目标,以及所有依赖它的目标 |
includes | 字符串列表 为C++编译器传递的头文件包含目录,实际上会前缀以-isystem并添加到COPTS。与copts属性不同,这些头文件包含会影响当前目标,以及所有依赖它的目标 如果不清楚有何副作用,可以传递-I到copts,而不是使用当前属性 |
linkopts | 字符串列表 为C++链接器传递选项,在链接二进制文件之前,此属性中的每个字符串被添加到LINKOPTS 此属性列表中,任何不以$和-开头的项,都被认为是deps中声明的某个目标的Label,目标产生的文件会添加到链接选项中 |
linkshared | 布尔,默认False。用于创建共享库 要创建共享库,指定属性linkshared = True,对于GCC来说,会添加选项-shared。生成的结果适合被Java这类应用程序加载 需要注意,这里创建的共享库绝不会被链接到依赖它的二进制文件,而只适用于被其它程序手工的加载。因此,不能代替cc_library 如果同时指定 linkopts=['-static']和linkshared=True,你会得到一个完全自包含的单元。如果同时指定linkstatic=True和linkshared=True会得到一个基本是完全自包含的单元 |
linkstatic | 布尔,默认True 对于cc_binary和cc_test,以静态形式链接二进制文件。对于cc_binary此选项默认True,其它目标默认False 如果当前目标是binary或test,此选项提示构建工具,尽可能链接到用户库的.a版本而非.so版本。某些系统库可能仍然需要动态链接,原因是没有静态库,这导致最终的输出仍然使用动态链接,不是完全静态的 链接一个可执行文件时,实际上有三种方式:
对于cc_library来说,linkstatic属性的含义不同。对于C++库来说:
|
malloc | 指向标签,默认@bazel_tools//tools/cpp:malloc 覆盖默认的malloc依赖,默认情况下C++二进制文件链接到//tools/cpp:malloc,这是一个空库,这导致实际上链接到libc的malloc |
nocopts | 字符串 从C++编译命令中移除匹配的选项,此属性的值是正则式,任何匹配正则式的、已经存在的COPTS被移除 |
stamp | 整数,默认-1 用于将构建信息嵌入到二进制文件中,可选值:
|
toolchains | 标签列表 提供构建变量(Make variables,这些变量可以被当前目标使用)的工具链的标签列表 |
win_def_file | 标签 传递给链接器的Windows DEF文件。在Windows上,此属性可以在链接共享库时导出符号 |
cc_import
导入预编译好的C/C++库。
属性列表:
属性 | 说明 |
hdrs | 此预编译库对外发布的头文件列表,依赖此库的规则(dependent rule)会直接将这些头文件包含在源码列表中 |
alwayslink | 布尔,默认False 如果为True,则依赖此库的二进制文件会将此静态库归档中的对象文件链接进去,就算某些对象文件中的符号并没有被二进制文件使用 |
interface_library | 用于链接共享库时使用的接口(导入)库 |
shared_library | 共享库,Bazel保证在运行时可以访问到共享库 |
static_library | 静态库 |
system_provided | 提示运行时所需的共享库由操作系统提供,如果为True则应该指定interface_library,shared_library应该为空 |
cc_library
对于所有cc_*规则来说,构建所需的任何头文件都要在hdrs或srcs中声明。
对于cc_library规则,在hdrs声明的头文件构成库的公共接口。这些头文件可以被当前库的hdrs/srcs中的文件直接包含,也可以被依赖(deps)当前库的其它cc_*的hdrs/srcs直接包含。位于srcs中的头文件,则仅仅能被当前库的hdrs/srcs包含。
cc_binary和cc_test不会暴露接口,因此它们没有hdrs属性。
属性列表:
属性 | 说明 |
name | 库的名称 |
deps | 需要链接到(into)当前库的其它库 |
srcs | 头文件和源码列表 |
hdrs | 导出的头文件列表 |
copts/nocopts | 传递给C++编译命令的参数 |
defines | 宏定义列表 |
include_prefix | hdrs中头文件的路径前缀 |
includes | 字符串列表 需要添加到编译命令的包含文件列表 |
linkopts | 链接选项 |
linkstatic | 是否生成动态库 |
strip_include_prefix | 字符串 需要脱去的头文件路径前缀,也就是说使用hdrs中头文件时,要把这个前缀去除,路径才匹配 |
textual_hdrs | 标签列表 头文件列表,这些头文件是不能独立编译的。依赖此库的目标,直接以文本形式包含这些头文件到它的源码列表中,这样才能正确编译这些头文件 |
常见用例
通配符
可以使用Glob语法为目标添加多个文件:
cc_library( name = "build-all-the-files", srcs = glob(["*.cc"]), hdrs = glob(["*.h"]), )
传递性依赖
如果源码依赖于某个头文件,则该源码的规则需要dep头文件的库,仅仅直接依赖才需要声明:
# 三明治依赖面包 cc_library( name = "sandwich", srcs = ["sandwich.cc"], hdrs = ["sandwich.h"], # 声明当前包下的目标为依赖 deps = [":bread"], ) # 面包依赖于面粉,三明治间接依赖面粉,因此不需要声明 cc_library( name = "bread", srcs = ["bread.cc"], hdrs = ["bread.h"], deps = [":flour"], ) cc_library( name = "flour", srcs = ["flour.cc"], hdrs = ["flour.h"], )
添加头文件路径
有些时候你不愿或不能将头文件放到工作空间的include目录下,现有的库的include目录可能不符合
导入已编译库
导入一个库,用于静态链接:
cc_import( name = "mylib", hdrs = ["mylib.h"], static_library = "libmylib.a", # 如果为1则libmylib.a总会链接到依赖它的二进制文件 alwayslink = 1, )
导入一个库,用于共享链接(UNIX):
cc_import( name = "mylib", hdrs = ["mylib.h"], shared_library = "libmylib.so", )
通过接口库(Interface library)链接到共享库(Windows):
cc_import( name = "mylib", hdrs = ["mylib.h"], # mylib.lib是mylib.dll的导入库,此导入库会传递给链接器 interface_library = "mylib.lib", # mylib.dll在运行时需要,链接时不需要 shared_library = "mylib.dll", )
在二进制目标中选择链接到共享库还是静态库(UNIX):
cc_import( name = "mylib", hdrs = ["mylib.h"], # 同时声明共享库和静态库 static_library = "libmylib.a", shared_library = "libmylib.so", ) # 此二进制目标链接到静态库 cc_binary( name = "first", srcs = ["first.cc"], deps = [":mylib"], linkstatic = 1, # default value ) # 此二进制目标链接到共享库 cc_binary( name = "second", srcs = ["second.cc"], deps = [":mylib"], linkstatic = 0, )
包含外部库
你可以在WORKSPACE中调用new_*存储库函数,来从网络中下载依赖。下面的例子下载Google Test库:
# 下载归档文件,并让其在工作空间的存储库中可用 new_http_archive( name = "gtest", url = "https://github.com/google/googletest/archive/release-1.7.0.zip", sha256 = "b58cb7547a28b2c718d1e38aee18a3659c9e3ff52440297e965f5edffe34b6d0", # 外部库的构建规则编写在gtest.BUILD # 如果此归档文件已经自带了BUILD文件,则可以调用不带new_前缀的函数 build_file = "gtest.BUILD", # 去除路径前缀 strip_prefix = "googletest-release-1.7.0", )
构建此外部库的规则如下:
cc_library( name = "main", srcs = glob( # 前缀去除,原来是googletest-release-1.7.0/src/*.cc ["src/*.cc"], # 排除此文件 exclude = ["src/gtest-all.cc"] ), hdrs = glob([ # 前缀去除 "include/**/*.h", "src/*.h" ]), copts = [ # 前缀去除,原来是external/gtest/googletest-release-1.7.0/include "-Iexternal/gtest/include" ], # 链接到pthread linkopts = ["-pthread"], visibility = ["//visibility:public"], )
使用外部库
沿用上面的例子,下面的目标使用gtest编写测试代码:
cc_test( name = "hello-test", srcs = ["hello-test.cc"], # 前缀去除 copts = ["-Iexternal/gtest/include"], deps = [ # 依赖gtest存储库的main目标 "@gtest//:main", "//lib:hello-greet", ], )
外部依赖
Bazel允许依赖其它项目中定义的目标,这些来自其它项目的依赖叫做“外部依赖“。当前工作空间的WORKSPACE文件声明从何处下载外部依赖的源码。
外部依赖可以有自己的1-N个BUILD文件,其中定义自己的目标。当前项目可以使用这些目标。例如下面的两个项目结构:
/ home/ user/ project1/ WORKSPACE BUILD srcs/ ... project2/ WORKSPACE BUILD my-libs/
如果project1需要依赖定义在project2/BUILD中的目标:foo,则可以在其WORKSPACE中声明一个存储库(repository),名字为project2,位于/home/user/project2。然后,可以在BUILD中通过标签@project2//:foo引用目标foo。
除了依赖来自文件系统其它部分的目标、下载自互联网的目标以外,用户还可以编写自己的存储库规则(repository rules )以实现更复杂的行为。
WORKSPACE的语法格式和BUILD相同,但是允许使用不同的规则集。
Bazel会把外部依赖下载到 $(bazel info output_base)/external目录中,要删除掉外部依赖,执行:
bazel clean --expunge
外部依赖类型
Bazel项目
可以使用local_repository、git_repository或者http_archive这几个规则来引用。
引用本地Bazel项目的例子:
local_repository( name = "coworkers_project", path = "/path/to/coworkers-project", )
在BUILD中,引用coworkers_project中的目标//foo:bar时,使用标签@coworkers_project//foo:bar
非Bazel项目
可以使用new_local_repository、new_git_repository或者new_http_archive这几个规则来引用。你需要自己编写BUILD文件来构建这些项目。
引用本地非Bazel项目的例子:
new_local_repository( name = "coworkers_project", path = "/path/to/coworkers-project", build_file = "coworker.BUILD", )
cc_library( name = "some-lib", srcs = glob(["**"]), visibility = ["//visibility:public"], )
在BUILD文件中,使用标签 coworkers_project//:some-lib引用上面的库。
外部包
对于Maven仓库,可以使用规则maven_jar/maven_server来下载JAR包,并将其作为Java依赖。
依赖拉取
默认情况下,执行bazel Build时会按需自动拉取依赖,你也可以禁用此特性,并使用bazel fetch预先手工拉取依赖。
使用代理
Bazel可以使用HTTPS_PROXY或HTTP_PROXY定义的代理地址。
依赖缓存
Bazel会缓存外部依赖,当WORKSPACE改变时,会重新下载或更新这些依赖。
.bazelrc
Bazel命令接收大量的参数,其中一部分很少变化,这些不变的配置项可以存放在.bazelrc中。
位置
Bazel按以下顺序寻找.bazelrc文件:
- 除非指定--nosystem_rc,否则寻找/etc/bazel.bazelrc
- 除非指定--noworkspace_rc,否则寻找工作空间根目录的.bazelrc
- 除非指定--nohome_rc,否则寻找当前用户的$HOME/.bazelrc
语法
元素 | 说明 |
import | 导入其它bazelrc文件,例如: import %workspace%/tools/bazel.rc |
默认参数 | 可以提供以下行: startup ... 启动参数 以上三类行,都可以出现多次 |
--config | 用于定义一组参数的组合,在调用bazel命令时指定--config=memcheck,可以引用名为memcheck的参数组。此参数组的定义示例: build:memcheck--strip=never--test_timeout=3600 |
扩展
所谓Bazel扩展,是扩展名为.bzl的文件。你可以使用load语句加载扩展中定义的符号到BUILD中。
构建阶段
一次Bazel构建包含三个阶段:
- 加载阶段:加载、eval本次构建需要的所有扩展、所有BUILD文件。宏在此阶段执行,规则被实例化。BUILD文件中调用的宏/函数,在此阶段执行函数体,其结果是宏里面实例化的规则被填充到BUILD文件中
- 分析阶段:规则的代码——也就是它的implementation函数被执行,导致规则的Action被实例化,Action描述如何从输入产生输出
- 执行阶段:执行Action,产生输出,测试也在此阶段执行
Bazel会并行的读取/解析/eval BUILD文件和.bzl文件。每个文件在每次构建最多被读取一次,eval的结果被缓存并重用。每个文件在它的全部依赖被解析之后才eval。加载一个.bzl文件没有副作用,仅仅是定义值和函数
宏
宏(Macro)是一种函数,用来实例化(instantiates)规则。如果BUILD文件太过重复或复杂,可以考虑使用宏,以便减少代码。宏的函数在BUILD文件被读取时就立即执行。BUILD被读取(eval)之后,宏被替换为它生成的规则。bazel query只会列出生成的规则而非宏。
编写宏时需要注意:
- 所有实例化规则的公共函数,都必须具有一个无默认值的name参数
- 公共函数应当具有docstring
- 在BUILD文件中,调用宏时name参数必须是关键字参数
- 宏所生成的规则的name属性,必须以调用宏的name参数作为后缀
- 大部分情况下,可选参数应该具有默认值None
- 应当具有可选的visibility参数
要在宏中实例化原生规则(Native rules,不需要load即可使用的那些规则),可以使用native模块:
# 该宏实例化一个genrule规则 def file_generator(name, arg, visibility=None): // 生成一个genrule规则 native.genrule( name = name, outs = [name + ".txt"], cmd = "$(location generator) %s > $@" % arg, tools = ["//test:generator"], visibility = visibility, )
使用上述宏的BUILD文件:
load("//path:generator.bzl", "file_generator") file_generator( name = "file", arg = "some_arg", )
执行下面的命令查看宏展开后的情况:
# bazel query --output=build //label genrule( name = "file", tools = ["//test:generator"], outs = ["//test:file.txt"], cmd = "$(location generator) some_arg > $@", )
规则
规则定义了为了产生输出,需要在输入上执行的一系列动作。例如,C++二进制文件规则以一系列.cpp文件为输入,针对输入调用g++,输出一个可执行文件。注意,从Bazel的角度来说,不但cpp文件是输入,g++、C++库也是输入。当编写自定义规则时,你需要注意,将执行Action所需的库、工具作为输入看待。
Bazel内置了一些规则,这些规则叫原生规则,例如cc_library、cc_library,对一些语言提供了基础的支持。通过编写自定义规则,你可以实现对任何语言的支持。
定义在.bzl中的规则,用起来就像原生规则一样 —— 规则的目标具有标签、可以出现在bazel query。
规则在分析阶段的行为,由它的implementation函数决定。此函数不得调用任何外部工具,它只是注册在执行阶段需要的Action。
自定义规则
在.bzl文件中,你可以调用rule创建自定义规则,并将其保存到全局变量:
def _empty_impl(ctx): # 分析阶段此函数被执行 print("This rule does nothing") empty = rule(implementation = _empty_impl)
然后,规则可以通过load加载到BUILD文件:
load("//empty:empty.bzl", "empty") # 实例化规则 empty(name = "nothing")
规则属性
属性即实例化规则时需要提供的参数,例如srcs、deps。在自定义规则的时候,你可以列出所有属性的名字和Schema:
sum = rule( implementation = _impl, attrs = { # 定义一个整数属性,一个列表属性 "number": attr.int(default = 1), "deps": attr.label_list(), }, )
实例化规则的时候,你需要以参数的形式指定属性:
sum( name = "my-target", deps = [":other-target"], ) sum( name = "other-target", )
如果实例化规则的时候,没有指定某个属性的值(且没指定默认值),规则的实现函数会在ctx.attr中看到一个占位符,此占位符的值取决于属性的类型。
使用default为属性指定默认值,使用 mandatory=True 声明属性必须提供。
默认属性
任何规则自动具有以下属性:deprecation, features, name, tags, testonly, visibility。
任何测试规则具有以下额外属性:args, flaky, local, shard_count, size, timeout。
特殊属性
有两类特殊属性需要注意:
- 依赖属性:例如attr.label、attr.label_list,用于声明拥有此属性的目标所依赖的其它目标
- 输出属性:例如attr.output、attr.output_list,声明目标的输出文件,较少使用
私有属性
某些情况下,我们会为规则添加具有默认值的属性,同时还想禁止用户修改属性值,这种情况下可以使用私有属性。
私有属性以下划线 _ 开头,必须具有默认值。
目标
实例化规则不会返回值,但是会定义一个新的目标。
规则实现
任何规则都需要提供一个实现函数。提供在分析阶段需要严格执行的逻辑。此函数不能有任何读写行为,仅仅用于注册Action。
实现函数具有唯一性入参 —— 规则上下文,通常将其命名为ctx。通过规则上下文你可以:
- 访问规则属性
- 获得输入输出文件的handle
- 创建Actions
- 通过providers向依赖于当前规则的其它规则传递信息
常见的命令
build
bazel build //path:object
Debug
bazel build //path:object -c dbg
查看依赖关系
bazel query --notool_deps --noimplicit_deps "deps(//path:object)" --output graph
Clean
bazel clean