Bazel&C++ 通关手册

目录

安装

Ubuntu

Bazelisk

概念和术语

Workspace

Package

Target

Label

Rule

BUILD文件

Dependency

bzl文件

构建C++ 项目

通过Bazel构建

查看依赖图

指定多个目标

使用多个包

如何引用目标

Starlark

变量

跨BUILD变量

Make变量

一般预定义变量

genrule预定义变量

输入输出路径变量

一般规则

规则列表

filegroup

test_suite

alias

config_setting

genrule

C++规则

规则列表

cc_binary

cc_import

cc_library

常见用例

通配符

传递性依赖

添加头文件路径

导入已编译库

包含外部库

使用外部库

外部依赖

外部依赖类型

Bazel项目

非Bazel项目

外部包

依赖拉取

使用代理

依赖缓存

.bazelrc

位置

语法

扩展

构建阶段

规则

自定义规则

规则属性

默认属性

特殊属性

私有属性

目标

规则实现

常见的命令

build

Debug

查看依赖关系

Clean


安装

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

工作空间是一个目录,它包含:

  1. 构建目标所需要的源码文件,以及相应的BUILD文件
  2. 指向构建结果的符号链接
  3. 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文件中,包括:

  1. 规则(Rule),指定输入集和输出集之间的关系,声明从输入产生输出的必要步骤。一个规则的输出可以是另外一个规则的输入
  2. 文件(File),可以分为两类:
    1. 源文件
    2. 自动生成的文件(Derived files),由构建工具依据规则生成
  1. 包组:一组包,包组用于限制特定规则的可见性。包组由函数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,后缀通常有:

  1. *_binary 用于构建目标语言的可执行文件
  2. *_test 用于自动化测试,其目标是可执行文件,如果测试通过应该退出0
  3. *_library 用于构建目标语言的库

Dependency

目标A依赖B,就意味着A在构建或执行期间需要B。所有目标的依赖关系构成非环有向图(DAG)称为依赖图。

距离为1的依赖称为直接依赖,大于1的依赖则称为传递性依赖。

依赖分为以下几种:

  1. srcs依赖:直接被当前规则消费的文件
  2. deps依赖:独立编译的模块,为当前规则提供头文件、符号、库、数据
  3. 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构建

第一步是创建工作空间。工作空间中包含以下特殊文件:

  1. WORKSPACE,此文件位于根目录中,将当前目录定义为Bazel工作空间
  2. 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提供了:

  1. 预定义变量,可以在任何规则中使用
  2. 自定义变量,在规则中定义。仅仅在依赖该规则的那些规则中,可以使用这些变量

仅仅那些标记为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

输出目录,如果:

  1. outs仅仅包含一个文件名,则展开为包含该文件的目录
  2. outs包含多个文件,则此变量展开为在genfiles树中,当前包的根目录

输入输出路径变量

下表中的变量以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

隐含输出:

  1. name.stripped,仅仅当显式要求才会构建此输出,针对生成的二进制文件运行strip -g以驱除debug符号。额外的strip选项可以通过命令行--stripopt=-foo传入
  2. 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列表中,则当前规则自动依赖于那个规则:

  1. 如果那个规则的输出是C/C++源文件,则它们被编译进当前目标
  2. 如果那个规则的输出是库文件,则被链接到当前目标

允许的文件类型:

  1. C/C++源码,扩展名.c, .cc, .cpp, .cxx, .c++, .C
  2. C/C++头文件,扩展名.h, .hh, .hpp, .hxx, .inc
  3. 汇编代码,扩展名.S
  4. 归档文件,扩展名.a, .pic.a
  5. 共享库,扩展名.so, .so.version,version为soname版本号
  6. 对象文件,扩展名.o, .pic.o
  7. 任何能够产生上述文件的规则

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版本。某些系统库可能仍然需要动态链接,原因是没有静态库,这导致最终的输出仍然使用动态链接,不是完全静态的

链接一个可执行文件时,实际上有三种方式:

  1. STATIC,使用完全静态链接特性。所有依赖都被静态链接,GCC命令示例:gcc -static foo.o libbar.a libbaz.a -lm
  2. STATIC,所有用户库静态链接(如果存在静态库版本),但是系统库(除去C/C++运行时库)动态链接,GCC命令示例:
    gcc foo.olibfoo.alibbaz.a-lm
  3. DYNAMIC,所有依赖被动态链接(如果存在动态库版本),GCC命令示例:gcc foo.olibfoo.solibbaz.so-lm

对于cc_library来说,linkstatic属性的含义不同。对于C++库来说:

  1. linkstatic=True表示仅仅允许静态链接,也就是不产生.so文件
  2. linkstatic=False表示允许动态链接,同时产生.a和.so文件

malloc

指向标签,默认@bazel_tools//tools/cpp:malloc

覆盖默认的malloc依赖,默认情况下C++二进制文件链接到//tools/cpp:malloc,这是一个空库,这导致实际上链接到libc的malloc

nocopts

字符串

从C++编译命令中移除匹配的选项,此属性的值是正则式,任何匹配正则式的、已经存在的COPTS被移除

stamp

整数,默认-1

用于将构建信息嵌入到二进制文件中,可选值:

  1. stamp = 1,将构建信息嵌入,目标二进制仅仅在其依赖变化时重新构建
  2. stamp = 0,总是将构建信息替换为常量值,有利于构建结果缓存
  3. stamp = -1 ,由--[no]stamp标记控制是否嵌入

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文件:

  1. 除非指定--nosystem_rc,否则寻找/etc/bazel.bazelrc
  2. 除非指定--noworkspace_rc,否则寻找工作空间根目录的.bazelrc
  3. 除非指定--nohome_rc,否则寻找当前用户的$HOME/.bazelrc

语法

元素

说明

import

导入其它bazelrc文件,例如: import %workspace%/tools/bazel.rc

默认参数

可以提供以下行:

startup ... 启动参数
common... 适用于所有命令的参数
command...为某个子命令指定参数,例如buildquery、

以上三类行,都可以出现多次

--config

用于定义一组参数的组合,在调用bazel命令时指定--config=memcheck,可以引用名为memcheck的参数组。此参数组的定义示例:

build:memcheck--strip=never--test_timeout=3600

扩展

所谓Bazel扩展,是扩展名为.bzl的文件。你可以使用load语句加载扩展中定义的符号到BUILD中。

构建阶段

一次Bazel构建包含三个阶段:

  1. 加载阶段:加载、eval本次构建需要的所有扩展、所有BUILD文件。宏在此阶段执行,规则被实例化。BUILD文件中调用的宏/函数,在此阶段执行函数体,其结果是宏里面实例化的规则被填充到BUILD文件中
  2. 分析阶段:规则的代码——也就是它的implementation函数被执行,导致规则的Action被实例化,Action描述如何从输入产生输出
  3. 执行阶段:执行Action,产生输出,测试也在此阶段执行

Bazel会并行的读取/解析/eval BUILD文件和.bzl文件。每个文件在每次构建最多被读取一次,eval的结果被缓存并重用。每个文件在它的全部依赖被解析之后才eval。加载一个.bzl文件没有副作用,仅仅是定义值和函数

宏(Macro)是一种函数,用来实例化(instantiates)规则。如果BUILD文件太过重复或复杂,可以考虑使用宏,以便减少代码。宏的函数在BUILD文件被读取时就立即执行。BUILD被读取(eval)之后,宏被替换为它生成的规则。bazel query只会列出生成的规则而非宏。

编写宏时需要注意:

  1. 所有实例化规则的公共函数,都必须具有一个无默认值的name参数
  2. 公共函数应当具有docstring
  3. 在BUILD文件中,调用宏时name参数必须是关键字参数
  4. 宏所生成的规则的name属性,必须以调用宏的name参数作为后缀
  5. 大部分情况下,可选参数应该具有默认值None
  6. 应当具有可选的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。

特殊属性

有两类特殊属性需要注意:

  1. 依赖属性:例如attr.label、attr.label_list,用于声明拥有此属性的目标所依赖的其它目标
  2. 输出属性:例如attr.output、attr.output_list,声明目标的输出文件,较少使用

私有属性

某些情况下,我们会为规则添加具有默认值的属性,同时还想禁止用户修改属性值,这种情况下可以使用私有属性。

私有属性以下划线 _ 开头,必须具有默认值。

目标

实例化规则不会返回值,但是会定义一个新的目标。

规则实现

任何规则都需要提供一个实现函数。提供在分析阶段需要严格执行的逻辑。此函数不能有任何读写行为,仅仅用于注册Action。

实现函数具有唯一性入参 —— 规则上下文,通常将其命名为ctx。通过规则上下文你可以:

  1. 访问规则属性
  2. 获得输入输出文件的handle
  3. 创建Actions
  4. 通过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

 

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ym影子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值