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 bạ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 thử nghiệm. 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.
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. Nhóm quy tắc Bazel Go sẽ 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 tất cả nhà phát triển trong một dự án đều xây dựng 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 xuống
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 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/examples
Dự á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 tệp:
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 trong hướng dẫn này. Mỗi giai đoạn đều 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 ứng dụng bằng bazel build
, sau đó chạy ứng dụng:
$ 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 và chạy 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.
Thông thường, bạn 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. Thao tác này cũng đánh dấu thư mục gốc của dự án, vì vậy, bạn sẽ chỉ ghi 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 dự án Bazel, nhưng bạn vẫn nên có một tệp để có thể tiếp tục sử dụng go get
và go mod tidy
cho việc 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 đề này trong một hướng dẫn khác.
Tệp MODULE.bazel
của chúng tôi 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 có tính nă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 ra, chứa các hàm băm và siêu dữ liệu khác về các phần phụ thuộc của chúng ta. Nó bao gồm các phần phụ thuộc ngầm do chính Bazel thêm vào, vì vậy, nó khá dài và chúng ta sẽ không thấy nó ở đây. Giống như go.sum
, bạn nên cam kết tệp MODULE.bazel.lock
với tính năng kiểm soát nguồn để đảm bảo mọi người trong dự án của bạn đều nhận được cùng một phiên bản của từng 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 về 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, các tệp BUILD.bazel
), vì vậy, điều quan trọng là bạn phải hiểu được chức năng của các tệp này.
Tệp BUILD
được viết bằng một ngôn ngữ kịch bản có tên là Starlark, một tập hợp con có giới hạn của Python.
Tệp BUILD
chứa danh sách mục tiêu. Mục tiêu là thứ mà Bazel có thể tạo, chẳng hạn như một tệp nhị phân, thư viện hoặc kiểm thử.
Một mục tiêu gọi một hàm quy tắc bằng danh sách 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 đường dẫn tệp nguồn (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
).
Quy tắc cho Bazel biết cách tạo một mục tiêu. Trong ví dụ của mình, chúng tôi đã 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 nhóm tệp đầu ra. Ví dụ: go_binary
xác định các thao tác 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ể xem 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 mệnh 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 câu nói hay trong danh sách các 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 của 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. 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 Bazel biết cách tạo gói này. Ở đây, chúng ta sử dụng go_library
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"]
.
Bạn có thể đặt visibility
trên mọi mục tiêu. Nó xác định những gói Bazel 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 mọi mục tiêu đều 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 cách:
$ 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ách sử dụ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 lệnh này bằng lệnh bên dưới.
bazel run //:print_fortune
Mục tiêu print_fortune
có một 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. Tệp này chứa "//fortune"
, một chuỗi nhãn đề cập đế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ó vẻ rườm rà vì các phần phụ thuộc cũng được chỉ định trong tệp nguồn, nhưng tính rõ ràng của Bazel mang lại cho nó một lợi thế. Bazel tạo một biểu đồ hành động chứa tất cả các lệnh, đầ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 kết quả của hành động vào bộ nhớ đệm 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ữ được tích hợp sẵn.
Hiểu nhãn
Nhãn là một chuỗi mà Bazel dùng để xác định mục tiêu hoặc tệp. Nhãn được 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 ví dụ, 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ên tệp).
Tên kho lưu trữ được viết giữa @
và //
, đồng thời đượ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 đề cập đế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à :
, đồng thời đượ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 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 của tệp này 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 đề cập đế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à đề cập đế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 (do đó, //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ể 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 hữu ích cho việc 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 bài 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 dùng biến fortunes
chưa xuất, nên cần được biên dịch vào 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 tôi 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 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, điều này hữu ích cho mã đã tạo.
Bạn có thể 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 trường hợp trùng tên không mong muốn: 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 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 kiểm thử. Bazel cũng sẽ tạo các mục tiêu không phải là 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 những gói không có kiểm thử.
$ bazel test //...
Kết luận và tài liệu đọ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 trong quá trình này.
- Để 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 các quy tắc Core Go.
- Để 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, hãy xem phần các 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.