Bazel note

Bazel的学习

  • 摘自知乎https://zhuanlan.zhihu.com/p/262497747
java_binary(
	name = "MyBinary",
	srcs = ["MyBinary.java"], deps = [":mylib", ],
)
java_library(
	name = "mylib",
	srcs = ["MyLibrary.java", "MyHelper.java"],
	visibility = ["//java/com/example/myproduct:__subpackages__"],
	deps = [
	"//java/com/example/common", 
	"//java/com/example/myproduct/otherlib",
	"@com_google_common_guava_guava//jar",
], )
  • 在Bazel中,BUILD文件定义了targets。上面的两个targets分别是java_binary和java_library. 每个target都对应着bazel能够创建的一种artifact. binary targets生成能够直接运行的二进制文件,library targets生成能够被其它binary或者library所使用的内容。每个target都有一个name定义它在命令行和其它target中应该如何被引用),srcs定义必须被编译的相关源文件以生成对应的target制品),以及deps定义前置必须先构建或链接的依赖)。依赖关系可以限制在当前package以内(e.g. MyBinary依赖于:mylib),也可以是在同一个源代码层级中的不同package(e.g. mylib依赖于//java/com/example/common),或者源代码层级之外的第三方artifact(e.g. mylib依赖于"@com_google_common_grava_grava//jar"). 每个源代码层级(source hierarchy)都被称为一个workspace,并由根目录下的一个WORKSPACE文件来标示。

和Ant一样,用户也需要使用bazel的命令行工具来进行构建。为了构建MyBinary这个target,用户需要执行
bazel build :MyBinary

在第一次运行上面命令时,Bazel会执行下列工作:
1. 解析当前workspace中每一个BUILD文件,创建各个制品(artifacts)之间的依赖图(graph of dependencies)
2. 使用上面创建的图来决定MyBinary的依赖转换关系(transitive dependencies),也即是MyBinary所依赖的每个target,及每个target依赖的其它target,以此递归
3. 根据具体定义,按顺序构建或下载每一个依赖。Bazel在这里首先构建没有任何依赖的target,并保持跟踪对于每个target来说还有哪些依赖需要构建。一旦一个target的所有依赖都已经构建好之后,Bazel就开始构建该target. 该过程持续到MyBinary的每一个依赖都被构建完成。
4. 链接所有上一步中所生成的依赖,构建MyBinary来生成最后的可执行二进制文件。

从上面看,似乎这跟前面基于任务的构建系统也没有两样。的确,最后结果是一样的二进制文件,过程也同样包括解析一堆执行步骤来找到依赖关系,然后顺序执行这些步骤。然而,这里存在一些根本性的差异。第一个是在第3步中,因为Bazel知道每个target只生成一个Java library,它就清楚它只需要运行java编译器,而不是一个用户定义的脚本,这样bazel就知道并发执行这些步骤是安全的。这一点就能在一个多核机器上带来指数级别的性能提升,而且这只可能在基于artifact的构建系统上才能实现,因为它才能控制自己的执行方式以提供并发计算时的安全保障。

当然,优点不局限于并发支持。另一点就是当程序员第二次执行 bazel build :MyBinary 的时候。如果并没有什么修改,bazel一秒之内就会结束,并告诉你target已经最新,无需重新构建。这也是得益于我们之前提到的函数式编程模式—Bazel知道每个target都只是执行一次java编译器而已,而jiava编译器的输出又决定于它的输入,因此只要输入不变,输出结果就可以被重用。这样的分析适用于所有层面。如果MyBinary.java发生改变,Bazel就知道需要重建MyBinary,但是mylib是可以重用的。如果//java/com/example/common中的源文件发生改变,bazel也知道如何重建这个库,mylib以及MyBinary,但却可以重用//java/com/example/myproduct/otherlib. 因为Bazel知道每一步中所使用的工具属性,它就能够决定每一次执行时需要重构的最小制品组合(minimum set of artifacts),同时保证不会出现过期的结果。

其它Bazel相关技巧(Other Nifty Bazel Tricks)

基于Artifact的构建系统基本解决了并发和重用的问题,但这里还有些我们之前提过的难题。在我们讨论更多场景之前,我们来看看Bazel如何巧妙地处理这些问题的。

  • 对工具的依赖(Tools as dependencies)

在前面我们谈到一个问题,很多构建都依赖于安装在我们自己机器上的各种工具,由于工具版本和安装环境等因素,实现跨机器的构建就会变得比较困难。如果你的项目还是用了不同语言,并针对不同平台进行构建或编译,而每个平台都要求略有差异的工具来完成相思工作,这个问题就更加明显。(这里涉及到两个问题:环境依赖和平台针对性)

Bazel处理第一个问题的方式是把工具作为target所依赖的一部分。当前workspace中每个java_library都默认依赖于一个java编译器,但也可以在workspace层面进行配置。每次当Bazel构建一个java_library的时候,它首先检查特定的编译器是否在已知位置存在,如果没有的话首先下载。和其它依赖一样,如果java编译器发生变化,所有依赖于它的artifact都需要重建。Bazel中定义的每一类target都是用同样的策略来声明它需要运行的工具,确保无论什么样的环境下Bazel都能够正确初始化。

  • 扩展构建系统(Extending the build system)

Bazel本身自带对几种流行编程语言的支持,如Java, C++等,当然程序员的期望不止于此。当然,这部分也是因为基于任务的系统给了足够多的灵活性去支持任意的构建过程,所以我们也不打算在基于artifact的系统里就把这个特性放弃。幸运的是,Bazel允许通过自定义规则(custom rules)来扩展所支持的target类型

要定义一个Bazel的rule,开发者首先要定义rule需要的input(以BUILD文件中传递的参数形式)和该rule所生成的output。开发者还要定义该rule所要生成的actions. 每个action同样也要声明input和output,运行一个特定可执行文件或在文件中写入特定字符串,并能够通过input/output连接到其它的action. 这也意味着在Bazel里面,action是最底层的可编辑单元(lowest-level composable unit)–只要一个action只使用它所声明的input/output,它就能做任何它想做的事情,而Bazel则会负责对action进行规划安排并在合适的时候缓存其执行结果。

这样的系统并不是完全不会出问题的,它不可能去阻止一个程序员在action中做一些不正确的事情,例如引入一个结果不确定的过程等等。不过这种情况在实际操作中并不会经常出现,并且把滥用的可能性压制在最底层的action上,也大幅度降低了出现错误的可能。另外网上也有大量实现了对不同语言不同工具支持的rules,绝大部分项目实际上并不需要去定义他们自己的rules.即便是对不得不自定义rules的来说,所有的rule定义也只需要集中在一起,这意味着程序员可以随意使用这些rules而无需关注其实现细节。

  • 环境隔离(Isolating the environment)

无论如何,既然Actions可以做很多事情,那么听起来很可能和任务task一样会导致各种问题–例如两个actions难道不会也往一个文件写数据而导致冲突吗?实际上,Bazel利用了==沙盒技术(sandboxing)==而使得这种冲突是不可能的。在所支持的系统上,每一个action都通过文件沙盒技术(filesystem sandbox)相互隔离,每个action都只能看到一个有限的文件系统部分,这个部分包括了所有它所声明的inputs和生成的outputs. 这是依靠了和Docker一样的,基于Linux的LXC技术实现的。这意味着actions直接是不可能发生冲突,因为他们不可能读取自己没有声明的文件,而写往没有声明的文件则会在action完成后被立刻销毁。同时,Bazel也使用沙盒技术来限制action之间进行网络通讯。

  • 可确定的外部依赖(Making external dependencies deterministic)

这里还有一个问题没有解决:构建系统经常需要从外部下载依赖(工具或libraries),而不是直接去构建他们。这可以从前面示例中的 @com_google_common_guava_guava//jar 的依赖看出,这是从Maven下载一个jar包。

依赖当前workspace之外的文件是很有风险的。这些文件随时可能变化,也就导致构建系统需要频繁检查他们是否是最新版本。如果一个远程文件的变化没有同步到当前workspace中源代码来,这可能会导致无法复现的构建版本–这意味着一个构建可能今天正常,而明天就因为很隐晦的原因失败,仅仅是因为一个没有注意到的依赖发生了变化。最后,第三方外部依赖会带来严重的安全隐患。如果攻击者能够进入第三房的服务器,他们就能用自己的东西替换掉你所依赖的文件,从而让他们有可能控制你的构建环境和输出。

这里最根本的问题在于我们希望构建系统能够注意到这些外部文件,但同时又不需要把他们加入到代码控制(source control)中。更新一个依赖应该是一个有意识的行为,但这个行为应该由一个中心控制来一次完成,而不是由单独的程序员或者系统自动完成。这是因为我们希望构建本身是可确定的(deterministic),这意味着如果你check out上周的一个commit,你应该看到所有的依赖都和上周时一样,而不会仍然是今天的版本。

Bazel和其它构建系统通过workspace范围内的==清单(manifest)==来处理这个问题,这个清单列出了所有外部依赖的加密hash值(cryptographic hash)。这个hash值是一个相当简洁的方式来唯一标示外部依赖文件,而无需将文件本身提交到source control. 只要workspace中有任何新的外部依赖被引用时,该依赖的hash值就会加入到清单中,有可能是自动的,也可能是手动。当Bazel执行一个构建的时候,它会去检查缓存中的依赖(dependency)的hash值,和清单中的hash值做比较,如果不同的话则再去重新下载。

如果我们下载的artifact所具有的hash值也和清单中所声明的不同,那么构建就会失败,直到清单中的hash值根据实际情况进行调整。这可以自动完成,但这样的修改必须被审核通过,然后构建才能接受新的依赖。这意味着对于任何依赖调整,都会有一个对应的记录。同时,一个外部依赖不能在workspace的源文件没有变化的情况下就发生改变。这也意味着,当我们check out一个较老版本的源代码时,该构建一定会使用该版本提交时所使用的依赖,而如果这些依赖不再存在,构建也会失败。

当然,如果远程服务器不可用或使用错误的数据进行服务,这仍然会是一个问题。这会导致你所有的构建都会失败,除非你还有另一份复制的依赖数据可用。为了避免这个问题,我们推荐对于所有重要的项目,你应该把所有的依赖做一个镜像,并存在你所信任并能控制的服务器或服务上。否则哪怕是提交的hash值保证了安全性,你也仍然是把你的构建系统可用性寄托在第三房上。

  • 分布式构建(Distributed Builds)

google的代码库是非常庞大的—超过20亿行代码,相互之间的依赖链非常深,就算是最简单的二进制文件也可能依赖于上万个构建targets. 在这种规模下,几乎不可能在单机上在能接受的时间范围内完成构建,没有任何构建系统能够绕过机器硬件的物理限制。唯一可以考虑的办法让系统支持分布式构建(distributed builds),这样需要完成的单位工作就能分散到指定且可扩展的一组机器上。假设我们已经把系统工作拆分为足够小的单位,这就允许我们能够完成任何规模的构建。这样的可扩展性是我们通过定义基于artifact的构建系统所期望达到的圣杯。

  • 远程缓存(Remote Caching)

最简单的分布构建是仅仅实现远程缓存(remote caching),

为了引用一个依赖,Bazel 使用 label 语法对所有的包进行唯一标识,其格式如下:
@workerspace_name//path/of/package:target
比如,go 中常用的一个日志库 logrus 的 label 为:
@com_github_sirupsen_logrus//:go_default_library
如果是本项目中的包路径,可以将 // 之前的 workspace 名字省去。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值