Hướng dẫn về Bazel: Tạo một dự án Go

Báo cáo vấn đề Xem nguồn Nightly · 7.4 .

Hướng dẫn này giới thiệu cho bạn các kiến thức cơ bản về Bazel bằng cách chỉ cho 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 thư viện và chạy chương trình kiểm thử. 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 làm như vậy.

Bạn có thể kiểm tra xem đã cài đặt Bazel 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 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 tất cả nhà phát triển trên một bản dựng dự án đều sử dụng cùng một phiên bản Go.

Tuy nhiên, bạn vẫn nên cài đặt một chuỗi công cụ Go để chạy các lệnh như go getgo mod tidy.

Bạn có thể kiểm tra xem Go đã được cài đặt 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 kho lưu trữ Git, vì vậy, bạn cần cài đặt Git nếu chưa cài đặt này. Để 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ó ba thư mục con (stage1, stage2stage3), mỗi thư mục con cho một phần khác nhau của hướng dẫn này. Mỗi giai đoạn dựa trên giai đoạn trước.

Xây dựng cù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 này 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 Bazel biết chúng ta muốn xây dựng gì. Thông thường, bạn sẽ ghi 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 sẽ chỉ ghi một tệp MODULE.bazel cho mỗi dự án. Tệp này phục vụ mục đích tương tự như tệp go.mod của Go. Bạn thực sự không cần tệp go.mod trong dự án Bazel, nhưng vẫn nên có tệp này để có thể tiếp tục sử dụng go getgo 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 tôi 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 ta chứa một phần phụ thuộc duy nhất trên 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 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. 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 tôi sẽ không hiển thị tệp này tại đây. Giống như go.sum, bạn nên xác nhận tệp MODULE.bazel.lock của mình cho quyền kiểm soát nguồn để đảm bảo mọi người trong dự án của bạn 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 các 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 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 mục tiêu. Mục tiêu là một nội dung mà Bazel có thể tạo, chẳng hạn như 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 với danh sách thuộc tính để mô tả nội dung cần tạo. Ví dụ của chúng ta có hai 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, liên quan đến thư mục chứa tệp BUILD).

Một quy tắc cho Bazel biết cách tạo mục tiêu. Trong ví dụ này, 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 tập hợp tệp đầu ra. Ví dụ: go_binary xác định các thao tác biên dịch Go và liên kết để tạo tệp đầu ra có thể thực thi.

Bazel có các quy tắc tích hợp sẵn cho một số ngôn ngữ như Java và C++. Bạn có thể tìm thấy tài liệu về 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 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 lời chúc từ danh sách tin nhắn đượ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 lưu trữ 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 thủ quy ước này sẽ giúp bạn 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 tạo gói này cho Bazel. 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 để có thể nhập thư viện 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 bất kỳ mục tiêu nào. Tệp này 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:

$ 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. Đâ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ó 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 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 phải 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 các tệp nguồn, nhưng tính rõ ràng của Bazel mang lại lợi thế cho cách này. 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 kết quả 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 tích hợp dành riêng cho ngôn ngữ.

Hiểu nhãn

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 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 tính năng, chẳng hạn như //fortune, //:print-fortune@rules_go//go:def.bzl.

Nhãn có ba 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à được dùng để tham chiếu đến một mục tiêu từ một mô-đun Bazel khác (vì lý do trước đây, mô-đunkho lưu trữ đôi khi được dùng như đồng nghĩa). 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à đượ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 do tệp BUILD hoặc BUILD.bazel xác định 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 các gói của riêng 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à đề 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 (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ể dùng ... làm ký tự đại diện để tham chiếu đến tất cả mục tiêu trong một gói. Điều này rất hữu ích khi 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 để 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 chưa xuất, vì vậy, tệp này cần được biên dịch vào cùng một gói Go với 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.gofortune_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 tại đâ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 với go_testgo_binary, nhưng cũng hoạt động với go_library, đôi khi hữu ích cho mã được 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 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 lỗi xung đột 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ể tải bằng gói embed.

Hãy thử chạy kiểm thử của chúng ta 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 kiểm thử để có thể phát hiện lỗi biên dịch ngay cả trong các gói không có bài 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 đã xây dựng 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 của Bazel trong quá trình này.

  • Để bắt đầu xây dựng các ứng dụng khác bằng Bazel, hãy xem hướng dẫn về C++, Java, AndroidiOS.
  • 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 về 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 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 Go và chuỗi công cụ thông qua hệ thống mô-đun của Bazel, hãy xem phần Go với bzlmod.