分布式系统,程序语言,算法设计

Bazel 构建 Golang 项目

bazel 构建 golang

引子

Bazel 是一款谷歌开源的非常优秀的构建系统。它的定位,用官方的话来说是:

a fast, scalable, multi-language and extensible build system

大意为:

一款速度极快可伸缩跨语言并且可扩展的构建系统

使用 Bazel 构建 golang 项目,除了 Bazel 本身特性外,还需要了解针对 golang 的扩展包 rules_go。另外,可以使用 bazel gazelle 来进行一些自动生成的工作。

作者:青藤木鸟 https://www.qtmuniao.com, 转载请注明出处

Bazel 特点概要

对于官方给出的 Bazel 四个特点,下面来依次探讨。

快(Fast)

Bazel 的构建过程很快,它集合了之前构建系统的加速的一些常见做法。包括:

  1. 增量编译。只重新编译必须的部分,即通过依赖分析,只编译修改过的部分及其影响的路径。
  2. 并行编译。将没有依赖的部分进行并行执行,可以通过 --jobs 来指定并行流的个数,一般可以是你机器 CPU 的个数。遇到大项目马力全开时,Bazel 能把你机器的 CPU 各个核都吃满。
  3. 分布式/本地缓存。Bazel 将构建过程视为函数式的,只要输入给定,那么输出就是一定的。而不会随着构建环境的不同而改变(当然这需要做一些限制),这样就可以分布式的缓存/复用不同模块,这点对于超大项目的速度提升极为明显。

可伸缩(scalable)

Bazel 号称无论什么量级的项目都可以应对,无论是超大型单体项目(monorepo)、还是超多库的分布式项目(multirepo)。Bazel 还可以很方便的集成 CD/CI ,并在云端利用分布式环境进行构建。

它使用沙箱机制进行编译,即将所有编译依赖隔绝在一个沙箱中,比如编译 golang 项目时,不会依赖你本机的 GOPATH,从而做到同样源码、跨环境编译、输出相同,即构建的确定性。

跨语言(multi-language)

如果一个项目不同模块使用不同的语言,利用 Bazel 可以使用一致的风格来管理项目外部依赖和内部依赖。典型的项目如 Ray。该项目使用 C++ 构建 Ray 的核心调度组件、通过 Python/Java 来提供多语言的 API,并将上述所有模块用单个 repo 进行管理。如此组织使其项目整合相当困难,但 Bazel 在此处理的游刃有余,大家可以去该 repo 一探究竟。

可扩展(extensible)

Bazel 使用的语法是基于 Python 裁剪而成的一门语言:Startlark。其表达能力强大,往小了说,可以使用户自定义一些 rules (类似一般语言中的函数)对构建逻辑进行复用;往大了说,可以支持第三方编写适配新的语言或平台的 rules 集,比如 rules go。 Bazel 并不原生支持构建 golang 工程,但通过引入 rules go ,就能以比较一致的风格来管理 golang 工程。

Bazel 主要文件

使用 Bazel 管理的项目一般包含以下几种 Bazel 相关的文件:WORKSPACEBUILD(.bazel),*.bzl 和 *.bazelrc *等。其中 WORKSPACE 和 .bazelrc 放置于项目的根目录下,BUILD.bazel 放项目中的每个文件夹中(包括根目录), *.bzl 文件可以根据用户喜好自由放置,一般可放在项目根目录下的某个专用文件夹(比如 build)中。

WORKSPACE

  1. 定义项目根目录和项目名。
  2. 加载 Bazel 工具和 rules 集。
  3. 管理项目外部依赖库。

BUILD.(bazel)

该文件主要针对其所在文件夹进行依赖解析(label)和目标定义(bazel target)。拿 go 来说,构建目标可以是 go_binary、go_test、go_library 等。

Bazel 的之前版本用的文件名是 BUILD ,但是在一些大小写不区分的系统上,它很容易跟 build 文件混淆,因此后来改为了显式的 BUILD.bazel 。如果项目中同时存在两者,Bazel 更倾向于使用后者。对于所有的新项目,都推荐使用显式的 BUILD.bazel。github 上有一些讨论在这里

为了引用一个依赖,Bazel 使用 label 语法对所有的包进行唯一标识,其格式如下:

1
@workerspace_name//path/of/package:target

比如,go 中常用的一个日志库 logrus 的 label 为:

1
@com_github_sirupsen_logrus//:go_default_library

如果是本项目中的包路径,可以将 // 之前的 workspace 名字省去。

自定义 rule (*.bzl)

如果你的项目有一些复杂构造逻辑、或者一些需要复用的构造逻辑,那么可以将这些逻辑以函数形式保存在 .bzl 文件,供 WORKSPACE 或者 BUILD 文件调用。其语法跟 Python 类似:

1
2
3
4
5
6
7
8
9
10
def third_party_http_deps():
http_archive(
name = "xxxx",
...
)

http_archive(
name = "yyyy",
...
)

配置项 .bazelrc

其中 rc 后缀的命名方式是个计算机中经典的小习俗,感兴趣可以看看 StackOverflow 这个回答。简单的说,该文件用来配置对应的命令运行时的一些参数。常见的如 .vimrc,.bashrc 等。

对于 Bazel 来说,如果某些构建动作都需要某个参数,就可以将其写在此配置中,从而省去每次敲命令都重复输入该参数。举个 Go 的例子:由于国情在此,构建、测试和运行时可能都需要 GOPROXY,则可以配置如下:

1
2
3
4
# set GOPROXY
test --action_env=GOPROXY=https://goproxy.io
build --action_env=GOPROXY=https://goproxy.io
run --action_env=GOPROXY=https://goproxy.io

Bazel 构建 golang 项目

在有了上面 Bazel 的基础知识后,构建 golang 项目还需要了解两个概念:rules_go 和 bazel gazelle。

rules_go

rules_go 是一个 Bazel 的扩展包,Bazel 可以编译 Go。它由一系列的 rule 构成,包括 go_libray\go_binary\go_test,支持 vendor、交叉编译;可以方便集成 protobuf 、cgo 、gogo、nogo等工具。

它会在 Bazel 的沙箱中进行编译,不依赖本地 GOROOT/GOPATH,而是自动下载对应 Go 版本,从而可以在不同平台上进行一致性的编译。

bazel gazelle

Gazelle 是一个自动生成 Bazel 编译文件工具,包括给 WORKSPACE 添加外部依赖、扫描源文件依赖自动生成 BUILD.bazel 文件等。Gazelle 原生支持 Go 和 protobuf,当然可以通过扩展来支持其他语言和规则。Gazelle 可以使用 bazel 命令结合 gazelle rule 运行,也可以下载使用单独的 Gazelle 的命令行工具。

  • 自动添加外部依赖

bazel run //:gazelle update-repos repo-uri 可以从 go.mod 导入对应依赖包。

比如想要往项目中增加 Kafka 的 segmentio 的 go client 包,只需要在项目根目录下执行命令: bazel run //:gazelle update-repos github.com/segmentio/kafka-g

Gazelle 便会自动增加一条依赖到 WORKSPACE 文件:

1
2
3
4
5
6
go_repository(
name = "com_github_segmentio_kafka_go",
importpath = "github.com/segmentio/kafka-go",
sum = "h1:Mv9AcnCgU14/cU6Vd0wuRdG1FBO0HzXQLnjBduDLy70=",
version = "v0.3.4",
)
  • 自动生成构建文件

Gazelle 能够自动生成每个目录下的 BUILD.bazel 文件,只需要简单的两步:

  1. 在项目根目录的 BUILD.bazel 中配置加载配置 Gazelle:

    1
    2
    3
    4
    5
    6
    load("@bazel_gazelle//:def.bzl", "gazelle")

    # gazelle:prefix your/project/url
    gazelle(
    name = "gazelle",
    )

    需要注意的是 # 后面的内容对于 Bazel 而言是注释,对于 Gazelle 来说却是一种语法,会被 Gazelle 运行时所使用。当然 Gazelle 除了可以通过 bazel rule 运行,也可以单独在命令行中执行。

  2. 在根目录下执行 bazel run //:gazelle

一些实践

Bazel 有一些比较实用的实践,比如使用 http_archive 下载确定版本的外部依赖包、使用 stamp 变量注入、打包和发布等等。可以多去一些有很好的 Bazel 构建项目实践的开源项目中去看看: