Hướng dẫn này giới thiệu cho bạn những kiến thức cơ bản về Bazel bằng cách hướng dẫn cách tạo một dự án Go (Golang). Bạn sẽ tìm hiểu cách thiết lập không gian làm việc, tạo một chương trình nhỏ, nhập một thư viện và chạy chương trình kiểm thử của thư viện đó. Trong quá trình này, bạn sẽ tìm hiểu các khái niệm chính về Bazel, chẳng hạn như mục tiêu và tệp BUILD.
Thời gian hoàn thành ước tính: 30 phút
Trước khi bắt đầu
Cài đặt Bazel
Trước khi bắt đầu, trước tiên hãy cài đặt Bazel nếu bạn chưa cài đặt rồi.
Bạn có thể kiểm tra xem Bazel đã được cài đặt hay chưa bằng cách chạy bazel version trong bất kỳ thư mục nào.
Cài đặt Go (không bắt buộc)
Bạn không cần cài đặt Go để tạo các dự án Go bằng Bazel. Bộ quy tắc Bazel Go tự động tải xuống và sử dụng một chuỗi công cụ Go thay vì sử dụng chuỗi công cụ được cài đặt trên máy của bạn. Điều này đảm bảo rằng tất cả nhà phát triển trong một dự án đều tạo bằng cùng một phiên bản Go.
Tuy nhiên, bạn vẫn có thể muốn cài đặt một chuỗi công cụ Go để chạy các lệnh như go
get và go mod tidy.
Bạn có thể kiểm tra xem Go đã được cài đặt hay chưa bằng cách chạy go version trong bất kỳ thư mục nào.
Tải dự án mẫu
Các ví dụ về Bazel được lưu trữ trong một kho lưu trữ Git, vì vậy, bạn cần cài đặt Git nếu bạn chưa cài đặt. Để tải kho lưu trữ ví dụ xuống, hãy chạy lệnh sau:
git clone https://github.com/bazelbuild/examplesDự án mẫu cho hướng dẫn này nằm trong thư mục examples/go-tutorial.
Xem nội dung của dự án:
go-tutorial/
└── stage1
└── stage2
└── stage3
Có 3 thư mục con (stage1, stage2 và stage3), mỗi thư mục dành cho một phần khác nhau của hướng dẫn này. Mỗi giai đoạn được xây dựng dựa trên giai đoạn trước đó.
Tạo bằng Bazel
Bắt đầu trong thư mục stage1, nơi chúng ta sẽ tìm thấy một chương trình. Chúng ta có thể tạo chương trình đó bằng bazel build, sau đó chạy chương trình:
$ 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! 💚
Chúng ta cũng có thể tạo chương trình bằng một lệnh bazel run duy nhất:
$ 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! 💚
Tìm hiểu cấu trúc dự án
Hãy xem dự án mà chúng ta vừa tạo.
hello.go chứa mã nguồn Go cho chương trình.
package main
import "fmt"
func main() {
fmt.Println("Hello, Bazel! 💚")
}
BUILD chứa một số hướng dẫn cho Bazel, cho biết những gì chúng ta muốn tạo.
Bạn thường sẽ viết một tệp như thế này trong mỗi thư mục. Đối với dự án này, chúng ta có một mục tiêu go_binary duy nhất tạo chương trình từ hello.go.
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "hello",
srcs = ["hello.go"],
)
MODULE.bazel theo dõi các phần phụ thuộc của dự án. Tệp này cũng đánh dấu thư mục gốc của dự án, vì vậy, bạn chỉ cần viết một tệp MODULE.bazel cho mỗi dự án. Tệp này có mục đích tương tự như tệp go.mod của Go. Bạn không thực sự cần tệp go.mod trong một dự án Bazel, nhưng tệp này vẫn có thể hữu ích để bạn có thể tiếp tục sử dụng go get và go mod tidy để quản lý phần phụ thuộc. Bộ quy tắc Bazel Go có thể nhập các phần phụ thuộc từ go.mod, nhưng chúng ta sẽ đề cập đến vấn đề đó trong một hướng dẫn khác.
Tệp MODULE.bazel của chúng ta chứa một phần phụ thuộc duy nhất vào
rules_go, bộ quy tắc Go. Chúng ta cần phần phụ thuộc này vì Bazel không hỗ trợ tích hợp cho Go.
bazel_dep(
name = "rules_go",
version = "0.50.1",
)
Cuối cùng, MODULE.bazel.lock là một tệp do Bazel tạo, chứa các giá trị băm và siêu dữ liệu khác về các phần phụ thuộc của chúng ta. Tệp này bao gồm các phần phụ thuộc ngầm ẩn do chính Bazel thêm vào, vì vậy, tệp này khá dài và chúng ta sẽ không hiển thị ở đây. Giống như go.sum, bạn nên cam kết tệp MODULE.bazel.lock của mình để kiểm soát nguồn nhằm đảm bảo mọi người trong dự án đều nhận được cùng một phiên bản của mỗi phần phụ thuộc. Bạn không cần phải chỉnh sửa MODULE.bazel.lock theo cách thủ công.
Tìm hiểu tệp BUILD
Hầu hết hoạt động tương tác của bạn với Bazel sẽ thông qua các tệp BUILD (hoặc
tương đương là các tệp BUILD.bazel), vì vậy, bạn cần hiểu rõ chức năng của các tệp này.
Các tệp BUILD được viết bằng một ngôn ngữ tập lệnh có tên là
Starlark, một tập hợp con giới hạn của Python.
Tệp BUILD chứa danh sách các
mục tiêu. Mục tiêu là thứ mà Bazel có thể tạo, chẳng hạn như tệp nhị phân, thư viện hoặc chương trình kiểm thử.
Một mục tiêu gọi một hàm quy tắc bằng danh sách các
thuộc tính để mô tả những gì
cần được tạo. Ví dụ của chúng ta có 2 thuộc tính: name xác định mục tiêu trên dòng lệnh và srcs là danh sách các đường dẫn tệp nguồn (được phân tách bằng dấu gạch chéo, tương ứng với thư mục chứa tệp BUILD).
Một quy tắc cho Bazel biết cách tạo một
mục tiêu. Trong ví dụ của chúng ta, chúng ta đã sử dụng quy tắc
go_binary. Mỗi quy tắc xác định các hành động
(lệnh) tạo ra một tập hợp các tệp đầu ra. Ví dụ: go_binary xác định các hành động biên dịch và liên kết Go tạo ra một tệp đầu ra có thể thực thi.
Bazel có các quy tắc tích hợp cho một số ngôn ngữ như Java và C++. Bạn có thể tìm thấy tài liệu của các quy tắc này trong Bách khoa toàn thư về bản dựng. Bạn có thể tìm thấy các bộ quy tắc cho nhiều ngôn ngữ và công cụ khác trên Sổ đăng ký trung tâm Bazel (BCR).
Thêm thư viện
Chuyển sang thư mục stage2, nơi chúng ta sẽ tạo một chương trình mới in ra vận may của bạn. Chương trình này sử dụng một gói Go riêng biệt làm thư viện chọn một vận may từ danh sách thông báo được xác định trước.
go-tutorial/stage2
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ └── fortune.go
└── print_fortune.go
fortune.go là tệp nguồn cho thư viện. Thư viện fortune là một gói Go riêng biệt, vì vậy, các tệp nguồn của thư viện này nằm trong một thư mục riêng biệt. Bazel không yêu cầu bạn giữ các gói Go trong các thư mục riêng biệt, nhưng đây là một quy ước mạnh mẽ trong hệ sinh thái Go và việc tuân theo quy ước này sẽ giúp bạn duy trì khả năng tương thích với các công cụ Go khác.
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))]
}
Thư mục fortune có tệp BUILD riêng cho biết cách Bazel tạo gói này. Chúng ta sử dụng go_library ở đây thay vì go_binary.
Chúng ta cũng cần đặt thuộc tính importpath thành một chuỗi mà thư viện có thể được nhập vào các tệp nguồn Go khác. Tên này phải là đường dẫn kho lưu trữ (hoặc đường dẫn mô-đun) được nối với thư mục trong kho lưu trữ.
Cuối cùng, chúng ta cần đặt thuộc tính visibility thành ["//visibility:public"].
visibility có thể được đặt trên bất kỳ
mục tiêu nào. Thuộc tính này xác định gói Bazel nào có thể phụ thuộc vào mục tiêu này. Trong trường hợp này, chúng ta muốn bất kỳ mục tiêu nào cũng có thể phụ thuộc vào thư viện này, vì vậy, chúng ta sử dụng giá trị đặc biệt //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"],
)
Bạn có thể tạo thư viện này bằng:
$ bazel build //fortune
Tiếp theo, hãy xem cách print_fortune.go sử dụng gói này.
package main
import (
"fmt"
"github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)
func main() {
fmt.Println(fortune.Get())
}
print_fortune.go nhập gói bằng cùng một chuỗi được khai báo trong thuộc tính
importpath của thư viện fortune.
Chúng ta cũng cần khai báo phần phụ thuộc này cho Bazel. Dưới đây là tệp BUILD trong thư mục stage2.
load("@rules_go//go:def.bzl", "go_binary")
go_binary(
name = "print_fortune",
srcs = ["print_fortune.go"],
deps = ["//fortune"],
)
Bạn có thể chạy tệp này bằng lệnh bên dưới.
bazel run //:print_fortune
Mục tiêu print_fortune có thuộc tính deps, một danh sách các mục tiêu khác mà mục tiêu này phụ thuộc vào. Thuộc tính này chứa "//fortune", một chuỗi nhãn tham chiếu đến mục tiêu
trong thư mục fortune có tên là fortune.
Bazel yêu cầu tất cả các mục tiêu khai báo rõ ràng các phần phụ thuộc của chúng bằng các thuộc tính như deps. Điều này có thể gây khó khăn vì các phần phụ thuộc cũng được chỉ định trong các tệp nguồn, nhưng tính rõ ràng của Bazel mang lại lợi thế cho Bazel. Bazel
tạo một biểu đồ hành động
chứa tất cả các lệnh, dữ liệu đầu vào và đầu ra trước khi chạy bất kỳ lệnh nào,
mà không cần đọc bất kỳ tệp nguồn nào. Sau đó, Bazel có thể lưu vào bộ nhớ đệm kết quả hành động hoặc gửi các hành động để thực thi từ xa mà không cần logic dành riêng cho ngôn ngữ tích hợp.
Hiểu nhãn
Một nhãn là một chuỗi mà Bazel sử dụng
để xác định một mục tiêu hoặc một tệp. Nhãn được sử dụng trong các đối số dòng lệnh và trong các thuộc tính tệp BUILD như deps. Chúng ta đã thấy một vài nhãn, chẳng hạn như //fortune, //:print-fortune và @rules_go//go:def.bzl.
Nhãn có 3 phần: tên kho lưu trữ, tên gói và tên mục tiêu (hoặc tệp).
Tên kho lưu trữ được viết giữa @ và // và được dùng để tham chiếu đến một mục tiêu từ một mô-đun Bazel khác (vì các lý do trước đây, mô-đun và kho lưu trữ đôi khi được dùng thay thế cho nhau). Trong nhãn,
@rules_go//go:def.bzl, tên kho lưu trữ là rules_go. Bạn có thể bỏ qua tên kho lưu trữ khi tham chiếu đến các mục tiêu trong cùng một kho lưu trữ.
Tên gói được viết giữa // và : và được dùng để tham chiếu đến một mục tiêu trong một gói Bazel khác. Trong nhãn @rules_go//go:def.bzl,
tên gói là go. Gói Bazel
package là một tập hợp các tệp và
mục tiêu được xác định bằng tệp BUILD hoặc BUILD.bazel trong thư mục cấp cao nhất.
Tên gói là một đường dẫn được phân tách bằng dấu gạch chéo từ thư mục gốc của mô-đun (chứa MODULE.bazel) đến thư mục chứa tệp BUILD. Một gói có thể bao gồm các thư mục con, nhưng chỉ khi các thư mục con đó không chứa các tệp BUILD xác định gói riêng của chúng.
Hầu hết các dự án Go đều có một tệp BUILD cho mỗi thư mục và một gói Go cho mỗi tệp BUILD. Bạn có thể bỏ qua tên gói trong nhãn khi tham chiếu đến các mục tiêu trong cùng một thư mục.
Tên mục tiêu được viết sau : và tham chiếu đến một mục tiêu trong một gói.
Bạn có thể bỏ qua tên mục tiêu nếu tên đó giống với thành phần cuối cùng của tên gói (vì vậy, //a/b/c:c giống với //a/b/c; //fortune:fortune giống
với //fortune).
Trên dòng lệnh, bạn có thể sử dụng ... làm ký tự đại diện để tham chiếu đến tất cả các mục tiêu trong một gói. Điều này rất hữu ích để tạo hoặc kiểm thử tất cả các mục tiêu trong một kho lưu trữ.
# Build everything
$ bazel build //...
Kiểm thử dự án
Tiếp theo, hãy chuyển đến thư mục stage3, nơi chúng ta sẽ thêm một chương trình kiểm thử.
go-tutorial/stage3
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│ ├── BUILD
│ ├── fortune.go
│ └── fortune_test.go
└── print-fortune.go
fortune/fortune_test.go là tệp nguồn kiểm thử mới của chúng ta.
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)
}
}
Tệp này sử dụng biến fortunes không được xuất, vì vậy, tệp này cần được biên dịch thành cùng một gói Go như fortune.go. Hãy xem tệp BUILD để biết cách hoạt động:
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"],
)
Chúng ta có một mục tiêu fortune_test mới sử dụng quy tắc go_test để biên dịch và liên kết một tệp thực thi kiểm thử. go_test cần biên dịch fortune.go và
fortune_test.go cùng với cùng một lệnh, vì vậy, chúng ta sử dụng thuộc tính embed
ở đây để kết hợp các thuộc tính của mục tiêu fortune vào
fortune_test. embed thường được dùng nhất với go_test và go_binary, nhưng cũng hoạt động với go_library, đôi khi hữu ích cho mã đã tạo.
Bạn có thể đang thắc mắc liệu thuộc tính embed có liên quan đến gói
embed của Go hay không, gói này được dùng để truy cập vào các tệp dữ liệu
được sao chép vào một tệp thực thi. Đây là một sự trùng lặp tên đáng tiếc: thuộc tính embed của rules_go được giới thiệu trước gói embed của Go. Thay vào đó, rules_go
sử dụng embedsrcs để liệt kê các tệp có thể được tải bằng gói embed.
Hãy thử chạy chương trình kiểm thử bằng 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.
Bạn có thể sử dụng ký tự đại diện ... để chạy tất cả các chương trình kiểm thử. Bazel cũng sẽ tạo các mục tiêu không phải là chương trình kiểm thử, vì vậy, điều này có thể phát hiện lỗi biên dịch ngay cả trong các gói không có chương trình kiểm thử.
$ bazel test //...
Kết luận và đọc thêm
Trong hướng dẫn này, chúng ta đã tạo và kiểm thử một dự án Go nhỏ bằng Bazel, đồng thời tìm hiểu một số khái niệm cốt lõi về Bazel.
- Để bắt đầu tạo các ứng dụng khác bằng Bazel, hãy xem hướng dẫn cho C++, Java, Android, và iOS.
- Bạn cũng có thể xem danh sách các quy tắc được đề xuất cho các ngôn ngữ khác.
- Để biết thêm thông tin về Go, hãy xem mô-đun rules_go, đặc biệt là tài liệu Quy tắc Go cốt lõi.
- Để tìm hiểu thêm về cách làm việc với các mô-đun Bazel bên ngoài dự án của bạn, hãy xem phần phụ thuộc bên ngoài. Cụ thể, để biết thông tin về cách phụ thuộc vào các mô-đun và chuỗi công cụ Go thông qua hệ thống mô-đun của Bazel, hãy xem phần Go với bzlmod.