bazel

转载自
什么是Bazel—教程、实例和优势
作者:方石剑
链接:https://juejin.cn/post/7120840097863303199
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Bazel是一个开源的构建工具,由谷歌开发,用于自动化大规模软件的构建过程。Pinterest、Adobe、SpaceX、Nvidia和LinkedIn等公司都使用它。在本教程中,你将了解什么是Bazel,它如何工作,以及它的重要好处。你还会学到如何为你的monorepo项目生成Bazel构建。

你为什么要使用Bazel?

Bazel的工作原理与MakeMavenGradle等其他构建工具类似。然而,与其他工具不同的是,Bazel是为具有多语言依赖性的项目量身定做的。

例如,你可以有一个Rust或Go的服务器,一个flutter的移动客户端,和一个Angular的网络客户端。在这种情况下,如果你要手动编写自己的构建文件,以迎合每种语言的生态系统,这可能是一项艰巨的任务。幸运的是,Bazel为你完成了所有繁重的工作:

bazel tutorial, bazel build

Bazel最吸引人的特点之一是你可以轻松地将它与你的项目的CI/CD挂钩。这可以帮助你提高团队的生产力,因为你可以产生更可靠的构建,定期和严格地测试你的软件。因此,你也可以很容易地出货和发布更强大的构建。

然而,Bazel不仅仅是处理多语言的依赖关系。让我们来探讨一些使它如此强大的好处。

Bazel有哪些优点?

以下是使Bazel成为出色的构建工具的关键优势。

可重复性:Bazel产生纯功能构建,你的输出文件严格依赖于你的输入。这给你的构建带来两个重要的特点。首先,你的构建是密封的,这意味着只有你明确提到的输入可以被你的构建步骤所读取。第二,你的构建是重复的,或者换句话说,是可复制的。如果你使用一组固定的输入,Bazel每次都会产生相同的构建。可重复的构建使调试你的构建操作更加方便。

Functional Builds in Bazel

与Docker和Kubernetes集成:归功于其多功能性,Bazel补充了现代平台和系统,如Docker和Kubernetes。例如,你可以为你的monorepo准备一个方便的Docker容器,其中包含一个web客户端和一堆微服务。然后你可以用它来启动一个类似于生产环境的测试环境。这样你就可以通过你的容器来测试你的Bazel构建。此外,你可以启用增量构建,并使用AWS Fargate或Kubernetes等协调引擎管理你的部署。

Deploying Bazel Build using Docker and Kubernetes

可扩展性:Bazel的发明源于谷歌的一个内部构建工具Blaze。在谷歌内部,Bazel处理包含超过10万个源文件的项目的构建。换句话说,Bazel非常适合拥有庞大代码库的大型项目。虽然它提倡单程序模式,但它也能轻松处理微服务架构。Dropbox使用Bazel来扩展他们的CI/CD管道,以减少对他们的提交执行的测试数量。Uber也采用了Bazel来扩展他们的Go monorepo。他们利用Bazel的密封式构建来支持增量构建的生成,以支持他们的分布式基础设施。

声明式编程:为项目编写构建配置应该尽可能简单。Bazel是使用Starlark构建的,这是一种源自Python的高级语言。因此,它为开发者提供了一种更方便的方式来编写构建配置和属性,这些配置和属性都是可读的。此外,Bazel的抽象性使开发者无需处理复杂的东西,如编译器和链接器。

平行性和缓存:大规模的构建工具必须是高性能的。Bazel使用缓存机制来加快你的构建速度。它智能地将你的后续构建与你以前的缓存构建进行比较,只构建那些开发者更新的文件。这确保了Bazel只把你的CPU资源花在构建那些需要重新构建的项目部分。Bazel还允许你以并行的方式生成并发的构建,以节省分布式代码库的时间。你可以在单台机器上,也可以在多台机器上远程生成并行构建。

庞大且不断增长的开源社区:Bazel的第一个版本是在2019年推出的,差不多有2年时间。从那时起,谷歌在两年的时间里推送了100多个版本,试图为开发者不断发展这个工具。Bazel的Github repo有17.4k颗星,在Stack Overflow上有超过2K个标记的问题和超过6K个搜索结果。这些微小但不断增长的数字表明,它在社区中获得了相当快的吸引力。

Bazel术语和基础知识解释

现在,让我们来看看Bazel使用的一些重要术语:

工作区

工作区通常是Bazel从你的源代码构建你的项目的构建文件的目录。它以嵌套的层次方式包含各种源文件。在工作区的最高层或根层,你的项目可能也有一个专门的WORKSPACE 文本文件。它包含了对你的项目所需的所有外部依赖的引用,以生成你的构建。准确地说,工作区是你的输入被提取并转换为输出以生成所需的构建文件的地方。

软件包

包是位于工作区顶级目录下的简单目录。它们包含你的构建文件,可以命名为BUILDBUILD.bazel ,以及其他相关文件和指定的依赖关系。一个包可以嵌套在另一个包中,因为你的源代码是以分层的方式组织的。考虑一下下面的工作目录。

css

复制代码

src/application/BUILD src/application/main/render.txt src/application/staging/BUILD

该应用程序是一个包,因为它包含自己的BUILD 文件。同样,staging 是另一个包,但由于它直接属于application ,所以可以被认为是一个子包。然而,main 不是一个子包,而只是你的application 包内的一个普通目录,因为它没有自己的BUILD 文件。

目标

你的包内的所有东西都可以被认为是目标。它包括你的团队的开发人员编写并添加到你的项目中的源文件或Bazel根据你的构建配置构建的生成文件。除了文件之外,目标通常涉及到管理你的输入文件和输出文件之间关系的规则。换句话说,目标规则规定了Bazel如何构建你的构建文件,它要采取的中间步骤以及它需要执行的每个可执行操作。你的目标规则的输入文件可以是源文件或生成文件。不过,你的输出文件总是生成文件,因为它是构建工具本身的结果。你可以指定你的目标规则,以连锁你的输入和输出,进行连续的构建操作。例如,你可以使用前一个步骤的生成文件作为未来另一个步骤的输入文件。

标签

一个目标的命名法被称为标签。它只是一种识别属于一个包的不同目标的方法,并将它们与相同或不同包中的其他目标区分开来。
考虑一下下面这个标签:

java

复制代码

@repo//application:application_source

上面的标签描述了一个包内的目标application_source 应用。同样地,在下面的例子中,main_binary 描述了在包application 下的子包main 内的一个目标:

ruby

复制代码

@repo//application/main:main_binary

两个目标都有不同的标签,你可以期望每个标签都能唯一地识别一个目标。

依赖关系

当你构建你指定的目标时,一个或多个目标在构建过程中可能依赖于另一个目标。后者是一种依赖关系。为了进一步解释,考虑两个目标Target1Target2 。假设Target1 在构建或执行时需要Target2 。我们可以把这种关系表达为一个有向无环图,表明在这个过程中Target1 是如何依赖于`Target12的。Bazel称其为依赖图,用于区分实际依赖和声明依赖。

构建文件

早些时候,我们说过,在一个WORKSPACE ,每个包都包含自己的BUILD 文件。构建文件包含使Bazel为你的项目生成所需构建的程序。它是用Starlark来评估的,包含一个由Bazel执行的顺序语句列表。构建文件包含你声明的规则,执行这些规则的函数,以及以简单语法形式要求的变量。

我们知道Bazel会缓存以前的构建文件以加快构建过程。每当你的源文件发生变化时,Bazel就会引用BUILD 文件来了解底层的变化。

命令

Bazel提供了一系列的命令,允许你执行某些操作。你可以运行的最简单的命令是检查Bazel是否成功安装在你的系统中:

css

复制代码

$ bazel --version bazel 4.0.0

其他重要的命令有:bazel build,bazel runbazel test 。build命令,顾名思义,是为指定的输入或目标建立你的输出文件:

yaml

复制代码

$ bazel build //foo INFO: Analyzed target //foo:foo (14 packages loaded, 48 targets configured). INFO: Found 1 target... Target //foo:foo up-to-date: bazel-bin/foo/foo INFO: Elapsed time: 9.905s, Critical Path: 3.25s INFO: Build completed successfully, 6 total actions

run命令运行你的输出文件,test命令相当于同时运行build和run命令:

Bazel Terminologies in a Build Workflow

用Bazel构建/编译代码

现在我们知道了常见的Bazel术语,让我们明白我们可以用Bazel构建或编译我们的代码,具体步骤如下。

1..bazelrc 文件

我们做的第一步是在一个专门的.bazelrc 文件中写入我们的构建选项。这些选项决定了每次Bazel构建你的项目时要考虑的设置。下面是一个典型的.bazelrc 文件的样子,如果你正在构建一个JavaScript项目:

lua

复制代码

build --disk_cache=~/.cache/bazel-disk-cache build --enable_platform_specific_config build --symlink_prefix=dist/ test --test_output=errors test:debug --test_output=streamed --test_strategy=exclusive --test_timeout=9999 --nocache_test_results --define=VERBOSE_LOGS=1 run:debug --define=VERBOSE_LOGS=1 -- --node_options=--inspect-brk build:debug --compilation_mode=dbg build --nolegacy_external_runfiles build --incompatible_strict_action_env run --incompatible_strict_action_env coverage --instrument_test_targets try-import %workspace%/.bazelrc.user

2.添加buildifier 依赖关系

为了确保你所有的构建文件都以类似的方式格式化,Bazel使用了一个名为buildifier 的依赖项。你可以通过运行,把它安装为你项目的开发时依赖:

css

复制代码

npm install --save-dev @bazel/buildifier

它可以确保你所有的构建文件看起来都是一样的,以便在你项目的各个构建步骤中保持一致性。当Bazel使用其缓存的工件将你当前的构建与以前的构建进行比较时,这还有助于加快你的构建过程。

3.创建目标

这是你编写Bazel需要构建的所有源代码的地方。还记得Bazel如何为你指定的目标构建输出文件吗?那基本上就是与你的项目有关的应用级代码。一旦你定义并声明你的目标,Bazel就知道在构建所需的输出时要使用哪些输入。

4.添加构建规则

当我们完成了我们的构建选项,我们需要使用一些规则来构建我们的项目。在这一步,你可以选择自动生成的规则或定义你自己的自定义规则。谨慎的做法是利用自动生成的规则,请记住,在Bazel的官方资源库里有少量的规则。我们在构建文件中定义这些规则。

5.构建/编译项目

在你指定了你的构建规则后,Bazel通常会接收你的构建文件作为输入并将其加载到分析器。接下来,它根据你的目标和依赖关系产生一个动作图,并在你声明的输入上借口构建动作。最后,它产生构建输出,并将其存储在缓存工件中,供后续构建使用。

Bazel Build Process

让我们在行动中扩展这个理论,逐步看到这个过程是如何让我们用Bazel构建一个JavaScript单体的。

用Bazel建立一个monorepo项目

为了开始,我们可以使用@bazel ,在一个新的NodeJS项目中直接设置Bazel:

kotlin

复制代码

npm init @bazel bazel_js_monorepo

下面是上述命令运行成功后,你应该在终端上得到的结果:

New Bazel Project

你会注意到,现在你的项目中已经有了buildifier ,还有其他所需的文件,如用于管理你的JavaScript依赖的package.json ,你的项目的WORKSPACE ,以及最重要的BUILD.bazel 文件。

New Bazel Project Workspace

你还需要安装Babel和其他相关的依赖项来转译你的JavaScript代码:

bash

复制代码

npm install @babel/core @babel/cli @babel/preset-env

如果你前往WORKSPACE 文件,你会看到它是如何阐明Bazel在构建你的项目时需要获取的依赖项的。你需要明确地告诉Bazel你将使用哪些规则来生成你的构建。为了简洁起见,我们将使用自动生成的规则。

进入你项目根部的BUILD.bazel 文件,添加以下一行:

perl

复制代码

load("@npm//@babel/cli:index.bzl", "babel")

因此,你也需要为你的项目配置Babel。在您的项目根目录下创建一个新的文件,名为es5.babelrc 文件。回顾一下,我们之前看到的.babelrc 文件的目的是描述我们的构建选项,在其中添加以下代码:

perl

复制代码

{ "sourceMaps": "inline", "presets": [ [ "@babel/preset-env", { "modules": "systemjs" } ] ] }

上面的配置类似于webpack将Babel添加到你的普通JavaScript项目中。

在添加你的构建规则之前,你需要向Bazel提供你的输入或源文件。本质上,这些输入是Bazel视为构建你的构建的目标。让我们在根目录下创建一个名为app.js 的文件。我们将创建一个简单的JavaScript类,名为User ,里面有一个将用户名字转换为大写字母的函数:

kotlin

复制代码

class User{ constructor(name){ this.name=name } uppercaseName(){ this.name=this.name.toUpperCase(); return this.name; } } const user=new User("FuzzySid") user.uppercaseName();

接下来,你需要声明这个文件作为Bazel构建的输入。之前,我们已经在BUILD.bazel 文件中加载了构建规则。让我们用这些规则来声明我们在BUILD.bazel 文件中构建的输入和输出:

ini

复制代码

babel( name = "compile", data = [ "app.js", "es5.babelrc", "@npm//@babel/preset-env", ], outs = ["app.es5.js"], args = [ "app.js", "--config-file", "./$(execpath es5.babelrc)", "--out-file", "$(execpath app.es5.js)", ], )

最后,你可以运行构建命令,让Bazel为你施展魔法:

arduino

复制代码

npm run build

你应该在终端看到以下输出:

Bazel Build Successful

在你的项目中,你现在应该有一个bazel-out 和一个dist 目录:

Bazel Build Output directories

你转换的JavaScript代码应该出现在dist/bin/app.es5.js 。功夫不负有心人,你已经成功地用Bazel编译了一个JavaScript单程序