本教學課程將介紹 Bazel 的基本概念,並說明如何建構 Go (Golang) 專案。您將瞭解如何設定工作區、建構小型程式、匯入程式庫,以及執行測試。您將在過程中學習到重要的 Bazel 概念,例如目標和 BUILD
檔案。
預計完成時間:30 分鐘
事前準備
安裝 Bazel
開始之前,如果您尚未安裝 bazel,請先完成這項作業。
您可以在任何目錄中執行 bazel version
,檢查是否已安裝 Bazel。
安裝 Go (選用)
您不需要安裝 Go,即可使用 Bazel 建構 Go 專案。Bazel Go 規則集會自動下載並使用 Go 工具鍊,而非使用電腦上安裝的工具鍊。這可確保專案中所有開發人員都使用相同版本的 Go 進行建構。
不過,您還是可以安裝 Go 工具鍊,執行 go
get
和 go mod tidy
等指令。
如要檢查 Go 是否已安裝,請在任何目錄中執行 go version
。
取得範例專案
Bazel 範例會儲存在 Git 存放區,因此如果您尚未安裝 Git,請先安裝。如要下載範例存放區,請執行下列指令:
git clone https://github.com/bazelbuild/examples
本教學課程的範例專案位於 examples/go-tutorial
目錄中。查看內容:
go-tutorial/
└── stage1
└── stage2
└── stage3
有三個子目錄 (stage1
、stage2
和 stage3
),每個子目錄都對應至本教學課程的不同部分。每個階段都會建立在先前的階段之上。
使用 Bazel 建構
從 stage1
目錄開始,我們會在其中找到一個程式。我們可以使用 bazel build
建構,然後執行:
$ cd go-tutorial/stage1/
$ bazel build //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
bazel-bin/hello_/hello
INFO: Elapsed time: 0.473s, Critical Path: 0.25s
INFO: 3 processes: 1 internal, 2 darwin-sandbox.
INFO: Build completed successfully, 3 total actions
$ bazel-bin/hello_/hello
Hello, Bazel! 💚
我們也可以使用單一 bazel run
指令建構並執行程式:
$ bazel run //:hello
bazel run //:hello
INFO: Analyzed target //:hello (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:hello up-to-date:
bazel-bin/hello_/hello
INFO: Elapsed time: 0.128s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/hello_/hello
Hello, Bazel! 💚
瞭解專案結構
請查看我們剛建構的專案。
hello.go
包含程式的 Go 原始碼。
package main
import "fmt"
func main() {
fmt.Println("Hello, Bazel! 💚")
}
BUILD
包含一些 Bazel 指令,可告知 Bazel 我們要建構的項目。您通常會在每個目錄中編寫類似的檔案。針對這個專案,我們有一個 go_binary
目標,可從 hello.go
建構我們的程式。
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "hello",
srcs = ["hello.go"],
)
MODULE.bazel
會追蹤專案的依附元件。它也會標示專案的根目錄,因此每個專案只會寫入一個 MODULE.bazel
檔案。其用途與 Go 的 go.mod
檔案相似。您其實不需要在 Bazel 專案中使用 go.mod
檔案,但還是建議您建立一個檔案,以便繼續使用 go get
和 go mod tidy
管理依附元件。Bazel Go 規則集可從 go.mod
匯入依附元件,但我們會在其他教學課程中介紹這項作業。
我們的 MODULE.bazel
檔案包含對 rules_go 的單一依附元件,也就是 Go 規則集。我們需要這個依附元件,因為 Bazel 沒有內建 Go 支援。
bazel_dep(
name = "rules_go",
version = "0.50.1",
)
最後,MODULE.bazel.lock
是 Bazel 產生的檔案,內含關於依附元件的雜湊和其他中繼資料。這會包含 Bazel 本身新增的隱含依附元件,因此內容相當長,我們不會在這裡顯示。就像 go.sum
一樣,您應將 MODULE.bazel.lock
檔案提交至來源控制項,確保專案中的每個人都能取得相同版本的各項依附元件。您不必手動編輯 MODULE.bazel.lock
。
瞭解 BUILD 檔案
您與 Bazel 的大部分互動都會透過 BUILD
檔案 (或等效的 BUILD.bazel
檔案) 進行,因此請務必瞭解這些檔案的功能。
BUILD
檔案是以 Starlark 編寫,這是 Python 的子集。
BUILD
檔案包含目標清單。目標是指 Bazel 可建構的項目,例如二進位檔、程式庫或測試。
目標會呼叫規則函式,並提供屬性清單,說明應建構的內容。我們的範例有兩個屬性:name
會在指令列上識別目標,而 srcs
則是來源檔案路徑的清單 (以斜線分隔,相對於包含 BUILD
檔案的目錄)。
規則會告知 Bazel 如何建構目標。在本範例中,我們使用了 go_binary
規則。每項規則都會定義產生一組輸出檔案的動作 (指令)。舉例來說,go_binary
會定義 Go 編譯和連結動作,產生可執行的輸出檔案。
Bazel 內建 Java 和 C++ 等語言的規則。您可以在Build 百科全書中找到相關說明文件。您可以在 Bazel Central Registry (BCR) 上找到許多其他語言和工具的規則集。
新增程式庫
接著,我們將轉往 stage2
目錄,建立可列印運勢的全新程式。這個程式使用獨立的 Go 套件做為程式庫,從預先定義的訊息清單中選取一則籤詩。
go-tutorial/stage2
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ └── fortune.go
└── print_fortune.go
fortune.go
是程式庫的來源檔案。fortune
程式庫是個別的 Go 套件,因此其原始檔案位於個別目錄中。Bazel 並未要求您將 Go 套件放在不同的目錄中,但這是 Go 生態系統中的強烈慣例,遵循這項慣例有助於您與其他 Go 工具保持相容性。
package fortune
import "math/rand"
var fortunes = []string{
"Your build will complete quickly.",
"Your dependencies will be free of bugs.",
"Your tests will pass.",
}
func Get() string {
return fortunes[rand.Intn(len(fortunes))]
}
fortune
目錄有專屬的 BUILD
檔案,可告知 Bazel 如何建構這個套件。我們在此使用 go_library
而非 go_binary
。
我們也需要將 importpath
屬性設為字串,以便將程式庫匯入其他 Go 來源檔案。這個名稱應為存放區路徑 (或模組路徑),與存放區內的目錄連結在一起。
最後,我們需要將 visibility
屬性設為 ["//visibility:public"]
。visibility
可設為任何目標。它會決定哪些 Bazel 套件可能會依附此目標。在本例中,我們希望任何目標都能依附這個程式庫,因此我們使用特殊值 //visibility:public
。
load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "fortune",
srcs = ["fortune.go"],
importpath = "github.com/bazelbuild/examples/go-tutorial/stage2/fortune",
visibility = ["//visibility:public"],
)
您可以使用以下方式建構這個程式庫:
$ bazel build //fortune
接著,我們來看看 print_fortune.go
如何使用這個套件。
package main
import (
"fmt"
"github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)
func main() {
fmt.Println(fortune.Get())
}
print_fortune.go
會使用 fortune
程式庫 importpath
屬性中宣告的字串,匯入套件。
我們也需要向 Bazel 宣告這個依附元件。以下是 stage2
目錄中的 BUILD
檔案。
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "print_fortune",
srcs = ["print_fortune.go"],
deps = ["//fortune"],
)
您可以使用下列指令執行此操作。
bazel run //:print_fortune
print_fortune
目標具有 deps
屬性,也就是依附於其他目標的清單。其中包含 "//fortune"
,這是一個標籤字串,可參照 fortune
目錄中名為 fortune
的目標。
Bazel 要求所有目標都必須明確使用 deps
等屬性宣告依附元件。由於相依性「也」會在來源檔案中指定,因此這項做法看起來可能很麻煩,但 Bazel 的明確性為其帶來優勢。在執行任何指令之前,Bazel 會建立包含所有指令、輸入和輸出的動作圖表,而不需要讀取任何來源檔案。接著,Bazel 就能快取動作結果,或傳送遠端執行的動作,而不需要使用內建的語言特定邏輯。
瞭解標籤
標籤是 Bazel 用來識別目標或檔案的字串。標籤會用於指令列引數和 BUILD
檔案屬性 (例如 deps
)。我們已經看到幾個,例如 //fortune
、//:print-fortune
和 @rules_go//go:def.bzl
。
標籤包含三個部分:存放區名稱、套件名稱和目標 (或檔案) 名稱。
存放區名稱會寫在 @
和 //
之間,用於參照來自不同 Bazel 模組的目標 (基於歷史原因,模組和存放區 有時會互相替代使用)。在標籤 @rules_go//go:def.bzl
中,存放區名稱為 rules_go
。在同一個存放區中參照目標時,可以省略存放區名稱。
套件名稱會寫在 //
和 :
之間,用於參照來自不同 Bazel 套件的目標。在標籤 @rules_go//go:def.bzl
中,套件名稱為 go
。Bazel 套件是一組檔案和目標,由頂層目錄中的 BUILD
或 BUILD.bazel
檔案定義。其套件名稱是從模組根目錄 (包含 MODULE.bazel
) 到包含 BUILD
檔案的目錄之間的斜線分隔路徑。套件可以包含子目錄,但前提是這些子目錄不得包含定義自身套件的 BUILD
檔案。
大多數的 Go 專案每個目錄都有一個 BUILD
檔案,每個 BUILD
檔案也有一個 Go 套件。參照同一個目錄中的目標時,可以省略標籤中的套件名稱。
目標名稱會寫在 :
後方,並指的是套件中的目標。如果目標名稱與套件名稱的最後一個元件相同,則可以省略目標名稱 (因此 //a/b/c:c
與 //a/b/c
相同;//fortune:fortune
與 //fortune
相同)。
在指令列中,您可以使用 ...
做為萬用字元,用來參照套件中的所有目標。這項功能可用於建構或測試儲存庫中的所有目標。
# Build everything
$ bazel build //...
測試專案
接下來,請移至 stage3
目錄,我們會在該目錄中新增測試。
go-tutorial/stage3
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ ├── fortune.go
│ └── fortune_test.go
└── print-fortune.go
fortune/fortune_test.go
是新的測試來源檔案。
package fortune
import (
"slices"
"testing"
)
// TestGet checks that Get returns one of the strings from fortunes.
func TestGet(t *testing.T) {
msg := Get()
if i := slices.Index(fortunes, msg); i < 0 {
t.Errorf("Get returned %q, not one the expected messages", msg)
}
}
這個檔案使用未匯出的 fortunes
變數,因此需要編譯至與 fortune.go
相同的 Go 套件。請查看 BUILD
檔案,瞭解運作方式:
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "fortune",
srcs = ["fortune.go"],
importpath = "github.com/bazelbuild/examples/go-tutorial/stage3/fortune",
visibility = ["//visibility:public"],
)
go_test(
name = "fortune_test",
srcs = ["fortune_test.go"],
embed = [":fortune"],
)
我們有一個新的 fortune_test
目標,使用 go_test
規則編譯及連結測試可執行檔。go_test
需要使用相同指令編譯 fortune.go
和 fortune_test.go
,因此我們在這裡使用 embed
屬性,將 fortune
目標的屬性納入 fortune_test
。embed
最常用於 go_test
和 go_binary
,但也適用於 go_library
,這有時對產生的程式碼很有幫助。
您可能會想知道 embed
屬性是否與 Go 的 embed
套件相關,該套件用於存取複製到可執行檔的資料檔案。這是不幸的名稱衝突:rules_go 的 embed
屬性是在 Go 的 embed
套件推出之前引進。相反地,rules_go 會使用 embedsrcs
列出可透過 embed
套件載入的檔案。
請嘗試使用 bazel test
執行測試:
$ bazel test //fortune:fortune_test
INFO: Analyzed target //fortune:fortune_test (0 packages loaded, 0 targets configured).
INFO: Found 1 test target...
Target //fortune:fortune_test up-to-date:
bazel-bin/fortune/fortune_test_/fortune_test
INFO: Elapsed time: 0.168s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
//fortune:fortune_test PASSED in 0.3s
Executed 0 out of 1 test: 1 test passes.
There were tests whose specified size is too big. Use the --test_verbose_timeout_warnings command line option to see which ones these are.
您可以使用 ...
萬用字元執行所有測試。Bazel 也會建構非測試的目標,因此即使套件中沒有測試,也能偵測到編譯錯誤。
$ bazel test //...
結論和延伸閱讀
在本教學課程中,我們使用 Bazel 建構及測試一個小型 Go 專案,並學習了一些 Bazel 核心概念。