[Bazel]构建Golang项目

时间:2022-07-23
本文章向大家介绍[Bazel]构建Golang项目,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
  • 1 `rules_go` 与 `gazelle`
  • 2 `go build` 到 `bazel build`
    • 2.1 初始化 Bazel 构建
    • 2.2 编译 Go 项目
    • 2.3 测试编译结果
    • 2.4 Go 环境变量设置
  • 3 我们需要 `go build` 到 `bazel build`?

1 rules_gogazelle

Bazel 支持很多内置的规则,语言相关规则有 ShellObjective-CC++Java,比如 sh_binarycc_binarycc_importcc_libraryjava_binaryjava_import等。但是 Go 编译内置规则没有支持,不过好在 Bazel 支持规则扩展,可以自定义 Go 相关规则,包括可以实现如 go_binarygo_librarygo_test等规则。而 `rules_go`[1] 就是 Bazel 官方维护的 Go Bazel 开源扩展规则。`gazelle`[2] 这个项目可以将 Go 项目转为 Bazel 方式构建,包括生成 BUILD.bazel 文件,根据 go.mod 文件自动生成下载依赖模块规则 go_repository。这里简单介绍下 rules_gogazelle 相关内容,更多可以参考官方相关文档。

rules_go 主要特性支持包括:

  • 构建库、二进制可执行文件、测试(go_library、go_binary、go_test)
  • Vendoring
  • cgo
  • 交叉编译
  • 通过 nogo[3] 进行构建时代码分析
  • Protocol buffers
    • proto_library
    • go_proto_library
    • go_proto_compiler
  • 远程执行

rules_go 的使用环境很简单:

  • Bazel
  • 如果使用 cgo,则需要本机上有 C/C++ 工具链,默认的 Bazel 会尝试自动配置工具链
  • Bash、patch、cat 和 PATH 中的其他一些 Unix 工具
  • 无需安装 Go 工具链,Bazel 会自动为每个项目下载最新版本,当然你也可以用 rules_go 里的工具链相关规则配置本地 Go 工具链或下载指定版本
    • go_register_toolchains(go_version="1.14.0") : 下载指定的 Go SDK,默认最新版本

有两种方式使用 gazelle

  1. gazelle 本身就是用 Go 实现的一个工具,通过 Go 的方式使用它。比如:
$ gazelle -go_prefix github.com/example/project

# Add or update a repository to latest version by import path
$ gazelle update-repos example.com/new/repo

# Add or update a repository to specified version/commit by import path
$ gazelle update-repos example.com/new/repo@v1.3.1

# 从 go.mod 导入存储库,修改和更新 Bazel 宏
$ gazelle update-repos -from_file=go.mod -to_macro=repositories.bzl%go_repositories

# 设为-prune=true时,gazelle 将删除 Gopkg.lock/go.mod 文件中不再具有等效存储库的 go_repository 规则
$ gazelle update-repos -prune=true -from_file=go.mod -to_macro=repositories.bzl%go_repositories

gazelle 可以将配置指令以形式于 # gazelle:{key} {value} 注释方式放于 Bazel 构建文件中(BUILD),从而省去每次命令行都需要键入过程。更多的键值对设置可以参见这里:https://github.com/bazelbuild/bazel-gazelle#directives 。

  1. gazelle 的另一种方式就是直接和 Bazel 集成使用,作为一个外部规则导入使用,WORKSPACE 文件中:。
http_archive(
    name = "bazel_gazelle",
    sha256 = "cdb02a887a7187ea4d5a27452311a75ed8637379a1287d8eeb952138ea485f7d",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
    ],
)

load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")

gazelle_dependencies()

然后在 Go 项目根目录的 BUILDBUILD.bazel 文件中:

load("@bazel_gazelle//:def.bzl", "gazelle")

# gazelle:prefix github.com/example/project
gazelle(name = "gazelle")

对于 gazelle 规则,指定 Go 项目前缀,还可以:

load("@bazel_gazelle//:def.bzl", "gazelle")

# 建议是以注释方式也保留,Go 工具方式和 Bazel 方式都可以使用
# gazelle:prefix github.com/example/project
gazelle(
    name = "gazelle",
    prefix = "github.com/example/project",
)

最后,这里示例根据 go.mod 自动生成依赖仓库下载代码和相关 BUILD.bazel 文件:

# 自动添加一个外部依赖项目(非 go.mod 导入)
$ bazel run tools/cli:gazelle update-repos {repo-uri}

# 生成 BUILD.bazel 文件
$ bazel run tools/cli:gazelle
# 生成的依赖仓库下载代码自动生成到 go_repositories.bzl 文件中,然后自动生成导入代码到 WORKSPACE 文件中
$ bazel run tools/cli:gazelle -- update-repos -from_file=tools/cli/go.mod -to_macro=go_repositories.bzl%go_repositories

2 go buildbazel build

2.1 初始化 Bazel 构建

  1. 项目根目录创建 WORKSPACEBUILD 文件,在 WORKSPACE 文件中导入 rules_gogazelle 规则:
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

# download rules_go
http_archive(
    name = "io_bazel_rules_go",
    sha256 = "8663604808d2738dc615a2c3eb70eba54a9a982089dd09f6ffe5d0e75771bc4f",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.23.6/rules_go-v0.23.6.tar.gz",
        "https://github.com/bazelbuild/rules_go/releases/download/v0.23.6/rules_go-v0.23.6.tar.gz",
    ],
)

# load rules_go
load("@io_bazel_rules_go//go:deps.bzl", "go_rules_dependencies", "go_register_toolchains")

go_rules_dependencies()

go_register_toolchains()

# download gazelle
http_archive(
    name = "bazel_gazelle",
    sha256 = "cdb02a887a7187ea4d5a27452311a75ed8637379a1287d8eeb952138ea485f7d",
    urls = [
        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.21.1/bazel-gazelle-v0.21.1.tar.gz",
    ],
)

# load gazelle
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")

gazelle_dependencies()

比如我们创建了一个 Go 的一个命令行工具项目,放于我们工程的 tools/cli 目录下,即 Go 项目 go.mod 文件在该目录下:

.
├── BUILD
├── WORKSPACE
└── tools
    └── cli
        ├── BUILD
        ├── go.mod
        └── cmd
            └── ota_packer
                └── main.go

tools/cli/BUILD 文件内容配置 gazelle:

load("@bazel_gazelle//:def.bzl", "gazelle")

# gazelle:prefix github.com/yicm/OtaPackageTool
gazelle(
    name = "gazelle",
    prefix = "github.com/yicm/OtaPackageTool",
) 

在整个项目(非Go项目)根目录执行运行 gazelle 生成相关依赖模块导入代码和 BUILD.bazel 文件:

# 根据 go.mod,将go_repository规则写入一个单独的宏文件并将其加载到 WORKSPACE 文件中
$ bazel run tools/cli:gazelle -- update-repos -from_file=tools/cli/go.mod -to_macro=go_repositories.bzl%go_repositories
# 生成包的 BUILD.bazel 文件
$ bazel run tools/cli:gazelle

tools/cli/cmd/ota_packer 目录中生成了 BUILD.bazel 文件:

load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")

go_library(
    name = "go_default_library",
    srcs = ["main.go"],
    importpath = "github.com/yicm/OtaPackageTool/cmd/ota_packer",
    visibility = ["//visibility:private"],
)

go_binary(
    name = "ota_packer",
    embed = [":go_default_library"],
    visibility = ["//visibility:public"],
)

这里就很清楚了,目的就是生成 ota_packer 可执行文件。而在项目根目录生成了 go_repositories.bzl 文件,并在 WORKSPACE 文件中调用了 go_repositories 宏:

go_repositories.bzl:

load("@bazel_gazelle//:deps.bzl", "go_repository")

def go_repositories():
    go_repository(
        name = "co_honnef_go_tools",
        importpath = "honnef.co/go/tools",
        sum = "h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=",
        version = "v0.0.1-2019.2.3",
    )
    ......

WORKSPACE:

......

load("//:go_repositories.bzl", "go_repositories")

# gazelle:repository_macro go_repositories.bzl%go_repositories
go_repositories()

2.2 编译 Go 项目

上面小节完成了 rules_gogazelle 的配置和 Go 项目自动转成 Bazel 方式编译。而完成转换后,编译 Go 项目就是 Bazel 的使用了:

# 构建 ota_packer 目标
$ bazel build tools/cli/cmd/ota_packer:ota_packer
# 构建项目下所有目标
$ bazel build //...

2.3 测试编译结果

$ bazel run tools/cli/cmd/ota_packer:ota_packer

2.4 Go 环境变量设置

通过在命令行选项 --action_env 可以设置环境变量,从而作用于 actions。比如设置 Go 代理:

--action_env=GOPROXY=https://goproxy.cn

.bazelrc 文件中,可以设置 buildtestrun 命令的命令行默认选项:

build --action_env=GOPROXY=https://goproxy.cn
test  --action_env=GOPROXY=https://goproxy.cn
run   --action_env=GOPROXY=https://goproxy.cn

3 我们需要 go buildbazel build

Bazel Go 规则集,可以让我们很方便地管理 Go 工具链和外部库,而无需依赖于本地安装的库。Bazel 地官方项目 Gazelle,可以用来生成 Go 和 Protocol Buffers 规则。借助 Gazelle,能够以最少的人工输入为 Go 项目中的大多数 Go 软件包生成 Bazel 规则。

Bazel 本身具有的构建特性包括分布式缓存和构建、增量构建,只有当我们的工程代码发生改变或某些依赖发生变化时,才会触发构建并更新缓存,从而对大型项目可以实现快速构建。且 Bazel 的沙箱特性,保证每个开发者的构建环境一致。

Go 本身的 Go Modules 依赖管理已经变得成熟,我们可以很方便的管理我们的依赖包和版本。当然,使用 Bazel Go Rules 的同时,我们还可以使用原生的 go build,即两种方式不会发生冲突。

所以从 go buildbazel build 是否有必要,需要根据你的项目来决定。

参考资料

[1]rules_go: https://github.com/bazelbuild/rules_go

[2]gazelle: https://github.com/bazelbuild/bazel-gazelle

[3]nogo: https://github.com/bazelbuild/rules_go/blob/master/go/nogo.rst