概念和术语
说明
本文档概述了Bazel构建源代码树形结构和Bazel中使用的术语。翻译来源:https://docs.bazel.build/versions/master/build-ref.html
介绍
Bazel使用被称为“工作区”的目录中的源代码构建软件。工作空间中的源文件以嵌套的包层次结构进行组织,其中每个包位于一个目录,其中包含一组相关的源文件和一个BUILD文件。BUILD文件指定可以从源文件构建哪些软件输出。
工作区(Workspace),软件包(packages)和目标(targets)
工作空间(Workspace)
工作区包含构建程序所需源文件的文件系统,同时包含链接到输入的文件夹。每个工作空间目录都有一个名为WORKSPACE
的文本文件,该文件可以为空,也可以包含 构建程序所需外部依赖项的的引用。
WORKSPACE
所在目录被视为工作区的根目录。因此,Bazel会忽略工作空间中子目录的WORKSPACE
文件(因为它们形成另一个工作空间)。
Bazel还支持将WORKSPACE.bazel
file作为WORKSPACE
文件的别名。如果两个文件都存在,WORKSPACE.bazel
将具有优先权。
储存库(Repositories)
代码在存储库中组织。包含WORKSPACE
文件的目录是主存储库的根目录,也称为@
。其他(外部)存储库在WORKSPACE
通过工作区规则定义。
Bazel工作空间规则记录在Build Encyclopedia 的“ 工作空间规则”部分中,以及嵌入式Starlark存储库规则文档中 。
由于外部存储库本身就是存储库,因此它们通常也包含一个WORKSPACE
文件。但是,Bazel将忽略这些 WORKSPACE
文件。需要注意的是,依赖传递的存储库不会被自动添加。
软件包(Packages)
储存库中组织代码的主要单位是软件包。软件包集合相关的文件并描述它们之间的依赖性。
位于工作空间的顶级目录下包含名为BUILD
或BUILD.bazel
文件的目录定义为一个程序包。软件包包括其目录中的所有文件及子目录,但不包含自身有BUILD文件的子目录。
例如,在下面的目录树中,有两个软件包my/app
,和子软件包my/app/tests
。请注意,这my/app/data
不是包,而是属于package的目录my/app
。
src/my/app/BUILD
src/my/app/app.cc
src/my/app/data/input.txt
src/my/app/tests/BUILD
src/my/app/tests/test.cc
目标(Targets)
软件包是容器。包的元素称为 target。大多数目标是文件 和规则这两种主要类型之一。另外,还有另一种目标,即 包装组,但数量却要少得多。
文件进一步分为两种。 源文件通常是在人们的努力下编写的,并检入到存储库中。 生成的文件(有时称为派生文件)未检入,而是由构建工具根据特定规则从源文件生成的。
第二类目标是规则。规则指定一组输入和一组输出文件之间的关系,包括从输入派生输出的必要步骤。规则的输出始终是生成的文件。规则的输入可能是源文件,但也可能是生成的文件。因此,一条规则的输出可能是另一条规则的输入,从而可以构建较长的规则链。
在大多数情况下,规则输入是源文件还是生成文件都是无关紧要的。重要的只是该文件的内容。这个事实使用规则生成的生成文件替换复杂的源文件变得很容易,例如,当手动维护高度结构化的文件的负担变得太累,而有人编写程序来派生该文件时,就会发生这种情况。该文件的使用者不需要更改。相反,生成的文件可以很容易地被仅具有本地更改的源文件替换。
规则的输入也可以包括其他规则。这种关系的确切含义通常非常复杂,并且依赖于语言或规则,但是从直观上来说很简单:C ++库规则A可能有另一个C ++库规则B用于输入。这种依赖性的结果是,在编译期间B的头文件对A可用,在链接期间B的符号对A可用,而在执行期间B的运行时数据对A可用。
所有规则的不变之处在于,规则生成的文件始终与规则本身属于同一包;无法将文件生成到另一个包中。但是,规则的输入来自另一个程序包并不少见。
软件包组是一组软件包,其目的是限制某些规则的可访问性。包组由package_group
功能定义 。它们具有两个属性:它们包含的软件包列表及其名称。引用它们的唯一允许方式是从visibility
规则的default_visibility
属性或从package
函数的 属性。它们不生成或使用文件。有关更多信息,请参考Build Encyclopedia的相应部分。
标签(Labels)
所有目标完全属于一个包。目标的名称称为其标签(label),典型的标签形式如下:
@myrepo//my/app/main:app_binary
在标签引用它所在的相同存储库的典型情况下,存储库名称可能会被忽略。因此,在@myrepo
此标签内通常写为
//my/app/main:app_binary
每个标签都有两部分,包名称(my/app/main
)和目标名称(app_binary
)。每个标签都唯一标识一个目标。标签有时以其他形式出现;如果省略冒号,则假定目标名称与程序包名称的最后一个组成部分相同,因此这两个标签是等效的:
//my/app
//my/app:app
诸如此类的简短标签//my/app
不要与软件包名称混淆。标签以开头//
,但包名称从不my/app
包含,因此包含//my/app
。(一个常见的误解是//my/app
指一个程序包或程序包中的所有目标;都不是真的。)
在BUILD文件中,可以省略label的package-name部分,也可以省略冒号。因此,在用于程序包的BUILD文件 my/app
(即//my/app:BUILD
)中,以下“相对”标签都是等效的:
//my/app:app
//my/app
:app
app
(按照惯例,文件中省略了冒号,规则中保留了冒号,但是在其他方面并不重要。)
类似地,在BUILD文件中,可以通过相对于软件包目录的未修饰名称来引用属于该软件包的文件:
generate.cc
testdata/input.txt
但是,从其他软件包或从命令行,这些文件目标必须始终通过其完整标签来引用,例如 //my/app:generate.cc
。
相对标签不能用于引用其他程序包中的目标。在这种情况下,必须始终指定完整的软件包名称。例如,如果源树同时包含该包 my/app
和该包 my/app/testdata
(即,这两个包中的每一个都有其自己的BUILD文件)。后者包含一个名为的文件testdepot.zip
。这里有两种方法(一种错误,一种正确)来引用此文件 //my/app:BUILD
:
testdata/testdepot.zip # Wrong: testdata is a different package.
//my/app/testdata:testdepot.zip # Right.
如果错误地引用testdepot.zip
了错误的标签(例如//my/app:testdata/testdepot.zip
或)//my:app/testdata/testdepot.zip
,则会从构建工具中收到一条错误消息,指出标签“跨越了包装边界”。您应该通过将冒号放在包含最里面的BUILD文件的目录之后来更正标签 //my/app/testdata:testdepot.zip
。
以开头的标签@//
是对主存储库的引用,即使从外部存储库也可以使用。因此@//a/b/c
与从//a/b/c
外部存储库引用时有所不同 。前者返回主存储库,而后者//a/b/c
在外部存储库本身中查找。当在主存储库中编写引用主存储库中的目标的规则时,这尤其重要,并将在外部存储库中使用这些规则。
标签的词汇规范
标签的语法是故意严格的,以禁止对shell具有特殊含义的元字符。这有助于避免无意的引号问题,并使得构建用于操纵标签的工具和脚本(例如Bazel Query Language)更加容易 。允许的目标名称的详细信息如下。
目标名称 //...:**target-name**
target-name
是包中目标的名称。规则名称是name
BUILD文件中规则声明中的属性值;文件名是其相对于包含BUILD文件的目录的路径名。目标名称必须完全由从集合a
– z
, A
– Z
,0
– 9
和标点符号中提取的字符组成!%-@^_
"#$&'()*-+,;<=>?[]{|}~/.。不要
..用来引用其他软件包中的文件;使用 代替。文件名必须是标准格式的相对路径名,这意味着它们既不能以斜杠开头或结尾(例如,并且被禁止),也不能包含多个连续的斜杠作为路径分隔符(例如)。同样,上级引用(
//packagename:filename/foo
foo/foo//bar
..)和当前目录引用(
./)。该规则的唯一例外是目标名称可以完全由'
.`'组成。
虽然通常使用/
文件目标的名称,但我们建议您避免在/
规则名称中使用。特别是当使用标签的缩写形式时,可能会使读者感到困惑。即使没有这样的包装 ,标签//foo/bar/wiz
始终是其简写; 即使该目标存在,它也不会引用。//foo/bar/wiz:wiz``foo/bar/wiz``//foo:bar/wiz
但是,在某些情况下使用斜杠比较方便,有时甚至是必要的。例如,某些规则的名称必须与它们的主要源文件匹配,该文件可以位于包的子目录中。
套件名称 //**package-name**:...
包的名称是包含其BUILD文件的目录的名称,相对于源树的顶级目录。例如:my/app
。软件包名称必须完全由从集合A
- Z
,a
- z
, 0
- 9
,' /
',' -
',' .
','和' _
' 得出的字符组成,并且不能以斜杠开头。
对于具有对其模块系统重要的目录结构的语言(例如Java),重要的是选择作为该语言有效标识符的目录名称。
尽管Bazel允许在构建根目录下使用软件包(例如//:foo
),但不建议这样做,项目应尝试使用描述性更强的软件包。
程序包名称不得包含substring //
,也不能以斜杠结尾。
规则
规则指定输入和输出之间的关系,以及构建输出的步骤。规则可以是许多不同种类或类中的一种,它们可以生成编译的可执行文件和库,测试可执行文件和其他支持的输出,如 Build Encyclopedia中所述。
每个规则都有一个由name
属性指定的字符串类型的名称。该名称必须是语法上有效的目标名称,按规定以上。在某些情况下,名称有些随意,而由规则生成的文件的名称更有趣。属实是正确的。在其他情况下,名称是有意义的:例如对于*_binary
和*_test
规则,规则名称确定由构建生成的可执行文件的名称。
cc_binary(
name = "my_app",
srcs = ["my_app.cc"],
deps = [
"//absl/base",
"//absl/strings",
],
)
每个规则都有一套属性 ; 给定规则的适用属性以及每个属性的重要性和语义是规则类别的函数;有关规则及其对应属性的列表,请参见构建百科全书。每个属性都有一个名称和一个类型。属性可以具有的一些常见类型是整数,标签,标签列表,字符串,字符串列表,输出标签,输出标签列表。并非在每个规则中都需要指定所有属性。因此,属性形成了一个从键(名称)到可选的,键入的值的字典。
srcs
许多规则中存在 的属性的类型为“标签列表”;其值(如果存在)是标签列表,每个标签都是作为该规则输入的目标名称。
outs
许多规则中存在 的属性的类型为“输出标签列表”;这与srcs
属性的类型相似,但是在两个重要方面有所不同。首先,由于规则的输出与规则本身属于同一包,因此输出标签不能包含包组件。它们必须采用上面显示的“相对”形式之一。其次,(普通)标签属性所隐含的关系与输出标签所隐含的关系相反:规则取决于其srcs
,而规则则取决于其outs
。因此,两种类型的标签属性将方向分配给目标之间的边缘,从而产生依赖图。
这个针对目标的有向无环图称为“目标图”或“构建依赖关系图”,并且是Bazel Query工具在其上运行的域。
构建文件
上一节对软件包,目标和标签以及构建依赖关系图进行了抽象描述。在本节中,我们将研究用于定义包的具体语法。
根据定义,每个软件包都包含一个BUILD文件,这是一个简短的程序。使用命令性语言Starlark评估BUILD文件 。它们被解释为语句的顺序列表。
通常,顺序确实很重要:例如,必须在使用变量之前定义变量。但是,大多数BUILD文件仅包含构建规则的声明,并且这些语句的相对顺序无关紧要。重要的是,到评估包完成时,将宣布哪些规则以及具有哪些值。当执行构建规则函数(例如)时cc_library
,它将在图形中创建一个新目标。以后可以使用标签来引用此目标。因此,在简单的BUILD文件中,可以在不更改行为的情况下自由地对规则声明进行重新排序。
为了鼓励代码和数据之间的清晰区分,BUILD文件不能包含函数定义,for
语句或 if
语句(但可以使用列表推导和if
表达式)。应该在.bzl
文件中声明函数。此外, BUILD文件中不允许使用*args
和**kwargs
参数。而是显式列出所有参数。
至关重要的是,Starlark中的程序无法执行任意I / O。这个不变性使得BUILD文件的解释是封闭的,即仅依赖于一组已知的输入,这对于确保构建可复制是必不可少的。
尽管从技术上说,它们是使用Latin-1字符集来解释的,但BUILD文件应仅使用ASCII字符写入。
由于只要基础代码的依赖性发生变化,就需要更新BUILD文件,因此它们通常由团队中的多个人维护。鼓励BUILD文件作者自由地使用注释来记录每个构建目标的作用(无论它是否打算供公众使用),并记录软件包本身的作用。
加载扩展
Bazel扩展名是以结尾的文件.bzl
。使用该load
语句从扩展名导入符号。
load("//foo/bar:file.bzl", "some_library")
此代码将加载文件foo/bar/file.bzl
并将some_library
符号添加 到环境中。这可用于加载新规则,函数或常量(例如,字符串,列表等)。可以通过使用附加参数来调用多个符号load
。参数必须是字符串文字(无变量),并且load
语句必须出现在顶层,即它们不能在函数体内。的第一个参数load
是 标识文件的标签.bzl
。如果是相对标签,则相对于包含当前bzl
文件的包(而非目录)进行解析 。load
语句中的相对标签应使用前导:
。 load
还支持别名,即您可以为导入的符号分配不同的名称。
load("//foo/bar:file.bzl", library_alias = "some_library")
您可以在一个load
语句中定义多个别名。此外,参数列表可以包含别名和常规符号名称。以下示例完全合法(请注意何时使用引号)。
load(":my_rules.bzl", "some_rule", nice_alias = "some_other_rule")
在.bzl
文件中,_
不会导出以开头的符号,也无法从另一个文件中加载符号。可见性不影响加载(尚未):您无需使用exports_files
即可使.bzl
文件可见。
构建规则的类型
大多数构建规则来自家庭,按语言分组。例如,cc_binary
,cc_library
和cc_test
分别是C ++的二进制文件,库和测试,生成规则。其他语言使用相同的命名方案,但前缀不同,例如java_*
对于Java。其中一些功能记录在 Build Encyclopedia中,但是任何人都可以创建新规则。
-
*_binary
规则以给定的语言构建可执行程序。生成后,可执行文件将以规则标签的相应名称驻留在生成工具的二进制输出树中,因此//my:program
将出现在(例如)处$(BINDIR)/my/program
。这样的规则还会创建一个runfiles目录,其中包含
data
属于该规则的属性中提及的所有文件,或者该文件在其依赖关系的传递性关闭中的任何规则;这组文件集中在一个位置,以便于部署到生产环境。 -
*_test
规则是规则的一种特殊化*_binary
,用于自动化测试。测试只是简单的程序,成功返回零。与二进制文件一样,测试也具有运行文件树,其下的文件是测试可以在运行时合法打开的唯一文件。例如,程序
cc_test(name='x', data=['//foo:bar'])
可以$TEST_SRCDIR/workspace/foo/bar
在执行期间打开并读取。(每种编程语言都有其自己的实用程序函数来访问的值$TEST_SRCDIR
,但它们均等同于直接使用环境变量。)不遵守该规则将导致在远程测试主机上执行测试时测试失败。 *_library
规则以给定的编程语言指定单独编译的模块。库可以依赖于其他库,二进制文件和测试可以依赖于具有预期的单独编译行为的库。
依存关系
如果在构建或执行时需要目标,则 目标A
取决于目标 。将取决于关系诱导 向无环图上的目标(DAG),我们称之为一个 依赖图。目标的直接依存关系是依存关系图中长度为1的路径可到达的其他目标。目标的 传递依存关系是通过图形中任意长度的路径所依赖的目标。 B``B``A
实际上,在构建的上下文中,有两个依赖关系图,实际依赖关系图和声明的依赖关系图 。在大多数情况下,两个图是如此相似,以至于不需要进行区分,但对于下面的讨论很有用。
实际和声明的依赖关系
一个目标X
是实际上取决于目标 Y
当且仅当Y
必须存在,并内置了最新的,以便X
于正确建立。“构建”可能意味着生成,处理,编译,链接,归档,压缩,执行或在构建期间例行发生的任何其他类型的任务。
当且仅当的包中存在从 到的依赖边时, 目标X
才声明对目标 Y
具有依赖关系。 X``Y``X
为了正确构建,实际依赖项A的图必须是已声明依赖项D的图的子图。也就是说,每对直接连接的节点的x --> y
在甲必须也可以直接连接在d。我们说 d是overapproximation的一个。
重要的是,不要过分逼近,因为多余的声明依赖关系可能会使生成速度变慢而二进制文件更大。
对于BUILD文件编写者而言,这意味着每条规则都必须明确声明其对构建系统的所有实际直接依赖关系,并且不再声明。不遵守该原理会导致未定义的行为:构建可能会失败,但是更糟糕的是,构建可能取决于某些先前的操作,或者目标恰好具有这些可传递声明的依赖项。构建工具会积极尝试检查缺少的依存关系并报告错误,但是不可能在所有情况下都完成此检查。
您无需(也不应)尝试列出间接导入的所有内容,即使A在执行时“需要”它也是如此。
在构建目标期间X
,构建工具将检查的依赖关系的整个可传递性关闭,X
以确保最终结果中反映了这些目标的任何更改,并根据需要重建中间件。
依赖项的传递性导致常见错误。通过粗心的编程,一个文件中的代码可以使用间接依赖关系(即声明的依赖关系图中的可传递但不是直接边缘)提供的代码。间接依赖项未出现在BUILD文件中。由于规则不直接取决于提供者,因此无法跟踪更改,如以下示例时间线所示:
1.首先,一切正常
package中的代码a
使用package中的代码b
。包中的代码b
使用包中的代码c
,因此可a
传递地依赖c
。
The code in package a
uses code in package b
. The code in package b
uses code in package c
, and thus a
transitively depends on c
.
a/BUILD
rule(
name = "a",
srcs = "a.in",
deps = "//b:b",
)
a/a.in
import b;
b.foo();
b/BUILD
rule(
name = "b",
srcs = "b.in",
deps = "//c:c",
)
b/b.in
import c;
function foo() {
c.bar();
}
| [图片上传失败...(image-ea0f72-1583474417950)]
Declared dependency graph
| [图片上传失败...(image-62d6e1-1583474417950)]
Actual dependency graph
|
The declared dependencies overapproximate the actual dependencies. All is well.
声明的依赖关系过于逼近实际依赖关系。一切都很好。
2.引入了潜在危害。
有人不小心在上面添加了代码a
,从而直接创建了对的实际依赖c
,但忘记声明了。
a/a.in
import b;
import c;
b.foo();
c.garply();
| [图片上传失败...(image-cd9880-1583437362875)]
声明的依赖图
| [图片上传失败...(image-c8b0b4-1583437362875)]
实际依赖图
|
声明的依赖关系不再与实际依赖关系近似。这可能会成立,因为两个图的传递闭包是相等的,但掩盖了一个问题:a
对具有实际但未声明的依赖性c
。
3.危害被发现
有人进行重构b
,使其不再依赖于 c
,无意间突破a
了自己的错。
b/BUILD
name = "b",
srcs = "b.in",
deps = "//d:d",
)
b/b.in
import d;
function foo() {
d.baz();
}
声明的依赖图
|实际依赖图
声明的依赖关系图现在是实际依赖关系的近似值,即使在传递关闭时也是如此;构建可能会失败。通过确保在BUILD文件中正确声明了步骤2中引入的从a
到的实际依赖关系,可以避免该问题c
。
依赖类型
大多数的生成规则有通用的依赖,指定不同类型的三个属性:srcs
,deps
和 data
。这些将在下面说明。另请参阅 “构建百科全书”中所有规则共有的属性。
许多规则还对规则的特定种类的依赖,例如附加属性compiler
,resources
等等,这些都在Build百科全书详细说明。
srcs
依存关系
一个或多个输出源文件的规则直接消耗的文件。
deps
依存关系
指向单独编译的模块的规则,这些模块提供头文件,符号,库,数据等。
data
依存关系
构建目标可能需要一些数据文件才能正确运行。这些数据文件不是源代码:它们不会影响目标的构建方式。例如,单元测试可以将函数的输出与文件的内容进行比较。在构建单元测试时,我们不需要文件;但是运行测试时确实需要它。这同样适用于在执行过程中启动的工具。
构建系统在一个隔离的目录中运行测试,在该目录中只有列为“数据”的文件可用。因此,如果二进制/库/测试需要运行某些文件,请在数据中指定它们(或包含它们的构建规则)。例如:
# I need a config file from a directory named env:
java_binary(
name = "setenv",
...
data = [":env/default_env.txt"],
)
# I need test data from another directory
sh_test(
name = "regtest",
srcs = ["regtest.sh"],
data = [
"//data:file1.txt",
"//data:file2.txt",
...
],
)
这些文件可通过相对路径使用 path/to/data/file
。在测试中,也可以通过将测试的源目录的路径和工作空间相对的路径(例如)连接起来来引用它们 ${TEST_SRCDIR}/workspace/path/to/data/file
。
使用标签引用目录
查看我们的BUILD
文件时,您可能会注意到一些data
标签引用目录。这些标签以/.
或/
类似结尾:
data = ["//data/regression:unittest/."] # don't use this
或者像这样:
data = ["testdata/."] # don't use this
或者像这样:
data = ["testdata/"] # don't use this
这似乎很方便,特别是对于测试(因为它允许测试使用目录中的所有数据文件)。
但是,请不要这样做。为了确保更改后正确的增量重建(和测试的重新执行),构建系统必须知道作为构建(或测试)输入的完整文件集。当您指定目录时,仅当目录本身更改时(由于添加或删除文件),构建系统才会执行重建,但是由于这些更改不会影响封闭目录,因此无法检测到对单个文件的编辑。不应将目录指定为构建系统的输入,而应显式或使用glob()
函数枚举其中包含的文件集 。(**
用于强制递归。) glob()
data = glob(["testdata/**"]) # use this instead
不幸的是,在某些情况下必须使用目录标签。例如,如果testdata
目录包含名称不符合严格标签语法的文件 (例如,它们包含某些标点符号),则文件的显式枚举或使用该 glob()
函数将产生无效的标签错误。在这种情况下,您必须使用目录标签,但要注意上述伴随错误重建的风险。
如果必须使用目录标签,请记住,您不能使用相对../
路径来引用父包。相反,请使用“ //data/regression:unittest/.
”之类的绝对路径。
请注意,目录标签仅对数据依赖项有效。如果您尝试将目录用作以外的参数中的标签data
,它将失败,并且您将收到(可能是隐秘的)错误消息。