Tutorial Bazel: Mem-build Project Go

Tutorial ini memperkenalkan Anda pada dasar-dasar Bazel dengan menunjukkan cara mem-build project Go (Golang). Anda akan mempelajari cara menyiapkan ruang kerja, mem-build program kecil, mengimpor library, dan menjalankan pengujiannya. Selama proses ini, Anda akan mempelajari konsep utama Bazel, seperti target dan file BUILD.

Perkiraan waktu penyelesaian: 30 menit

Sebelum memulai

Menginstal Bazel

Sebelum memulai, instal bazel terlebih dahulu jika Anda belum melakukannya.

Anda dapat memeriksa apakah Bazel telah diinstal dengan menjalankan bazel version di direktori mana pun.

Menginstal Go (opsional)

Anda tidak perlu menginstal Go untuk mem-build project Go dengan Bazel. Kumpulan aturan Bazel Go otomatis mendownload dan menggunakan toolchain Go, bukan menggunakan toolchain yang diinstal di mesin Anda. Hal ini memastikan semua developer dalam project mem-build dengan versi Go yang sama.

Namun, Anda mungkin masih ingin menginstal toolchain Go untuk menjalankan perintah seperti go get dan go mod tidy.

Anda dapat memeriksa apakah Go telah diinstal dengan menjalankan go version di direktori mana pun.

Mendapatkan project contoh

Contoh Bazel disimpan di repositori Git, jadi Anda harus menginstal Git jika Anda belum melakukannya. Untuk mendownload repositori contoh, jalankan perintah ini:

git clone https://github.com/bazelbuild/examples

Project contoh untuk tutorial ini ada di direktori examples/go-tutorial. Lihat kontennya:

go-tutorial/
└── stage1
└── stage2
└── stage3

Ada tiga subdirektori (stage1, stage2, dan stage3), masing-masing untuk bagian tutorial yang berbeda. Setiap tahap dibangun berdasarkan tahap sebelumnya.

Mem-build dengan Bazel

Mulai di direktori stage1, tempat kita akan menemukan program. Kita dapat mem-build-nya dengan bazel build, lalu menjalankannya:

$ 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! 💚

Kita juga dapat mem-build program dengan satu perintah 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! 💚

Memahami struktur project

Lihat project yang baru saja kita build.

hello.go berisi kode sumber Go untuk program.

package main

import "fmt"

func main() {
    fmt.Println("Hello, Bazel! 💚")
}

BUILD berisi beberapa petunjuk untuk Bazel, yang memberi tahu Bazel apa yang ingin kita build. Biasanya, Anda akan menulis file seperti ini di setiap direktori. Untuk project ini, kita memiliki satu target go_binary yang mem-build program dari hello.go.

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

go_binary(
    name = "hello",
    srcs = ["hello.go"],
)

MODULE.bazel melacak dependensi project Anda. File ini juga menandai direktori utama project Anda, sehingga Anda hanya akan menulis satu file MODULE.bazel per project. File ini memiliki tujuan yang sama dengan file go.mod Go. Anda sebenarnya tidak memerlukan file go.mod dalam project Bazel, tetapi mungkin tetap berguna untuk memilikinya sehingga Anda dapat terus menggunakan go get dan go mod tidy untuk pengelolaan dependensi. Kumpulan aturan Bazel Go dapat mengimpor dependensi dari go.mod, tetapi kita akan membahasnya dalam tutorial lain.

File MODULE.bazel kita berisi satu dependensi pada rules_go, kumpulan aturan Go. Kita memerlukan dependensi ini karena Bazel tidak memiliki dukungan bawaan untuk Go.

bazel_dep(
    name = "rules_go",
    version = "0.50.1",
)

Terakhir, MODULE.bazel.lock adalah file yang dibuat oleh Bazel yang berisi hash dan metadata lainnya tentang dependensi kita. File ini mencakup dependensi implisit yang ditambahkan oleh Bazel itu sendiri, sehingga cukup panjang, dan kita tidak akan menampilkannya di sini. Sama seperti go.sum, Anda harus melakukan commit file MODULE.bazel.lock ke kontrol sumber untuk memastikan semua orang dalam project Anda mendapatkan versi yang sama dari setiap dependensi. Anda tidak perlu mengedit MODULE.bazel.lock secara manual.

Memahami file BUILD

Sebagian besar interaksi Anda dengan Bazel akan dilakukan melalui file BUILD (atau file BUILD.bazel yang setara), jadi penting untuk memahami fungsinya.

File BUILD ditulis dalam bahasa skrip yang disebut Starlark, subkumpulan Python yang terbatas.

File BUILD berisi daftar target. Target adalah sesuatu yang dapat di-build oleh Bazel, seperti biner, library, atau pengujian.

Target memanggil fungsi aturan dengan daftar atribut untuk menjelaskan apa yang harus di-build. Contoh kita memiliki dua atribut: name mengidentifikasi target di command line, dan srcs adalah daftar jalur file sumber (dipisahkan dengan garis miring, relatif terhadap direktori yang berisi file BUILD).

Sebuah aturan memberi tahu Bazel cara mem-build target. Dalam contoh kita, kita menggunakan aturan go_binary. Setiap aturan menentukan tindakan (perintah) yang menghasilkan sekumpulan file output. Misalnya, go_binary menentukan tindakan kompilasi dan link Go yang menghasilkan file output yang dapat dieksekusi.

Bazel memiliki aturan bawaan untuk beberapa bahasa seperti Java dan C++. Anda dapat menemukan dokumentasi mereka di Build Encyclopedia. Anda dapat menemukan kumpulan aturan untuk banyak bahasa dan alat lainnya di Bazel Central Registry (BCR).

Menambahkan library

Pindah ke direktori stage2, tempat kita akan mem-build program baru yang mencetak keberuntungan Anda. Program ini menggunakan paket Go terpisah sebagai library yang memilih keberuntungan dari daftar pesan yang telah ditentukan.

go-tutorial/stage2
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│   ├── BUILD
│   └── fortune.go
└── print_fortune.go

fortune.go adalah file sumber untuk library. Library fortune adalah paket Go terpisah, sehingga file sumbernya berada di direktori terpisah. Bazel tidak mengharuskan Anda menyimpan paket Go di direktori terpisah, tetapi ini adalah konvensi yang kuat dalam ekosistem Go, dan mengikutinya akan membantu Anda tetap kompatibel dengan alat Go lainnya.

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))]
}

Direktori fortune memiliki file BUILD sendiri yang memberi tahu Bazel cara mem-build paket ini. Kita menggunakan go_library di sini, bukan go_binary.

Kita juga perlu menetapkan atribut importpath ke string yang dapat digunakan untuk mengimpor library ke file sumber Go lainnya. Nama ini harus berupa jalur repositori (atau jalur modul) yang digabungkan dengan direktori dalam repositori.

Terakhir, kita perlu menetapkan atribut visibility ke ["//visibility:public"]. visibility dapat ditetapkan pada target apa pun. Atribut ini menentukan paket Bazel mana yang dapat bergantung pada target ini. Dalam kasus ini, kita ingin target apa pun dapat bergantung pada library ini, jadi kita menggunakan nilai khusus //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"],
)

Anda dapat mem-build library ini dengan:

$ bazel build //fortune

Selanjutnya, lihat cara print_fortune.go menggunakan paket ini.

package main

import (
    "fmt"

    "github.com/bazelbuild/examples/go-tutorial/stage2/fortune"
)

func main() {
    fmt.Println(fortune.Get())
}

print_fortune.go mengimpor paket menggunakan string yang sama yang dideklarasikan dalam atribut importpath library fortune.

Kita juga perlu mendeklarasikan dependensi ini ke Bazel. Berikut adalah file BUILD di direktori stage2.

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

go_binary(
    name = "print_fortune",
    srcs = ["print_fortune.go"],
    deps = ["//fortune"],
)

Anda dapat menjalankannya dengan perintah di bawah.

bazel run //:print_fortune

Target print_fortune memiliki atribut deps, daftar target lain yang menjadi dependensinya. Target ini berisi "//fortune", string label yang merujuk ke target di direktori fortune yang bernama fortune.

Bazel mengharuskan semua target mendeklarasikan dependensinya secara eksplisit dengan atribut seperti deps. Hal ini mungkin tampak rumit karena dependensi juga ditentukan dalam file sumber, tetapi kejelasan Bazel memberikan keunggulan. Bazel mem-build grafik tindakan yang berisi semua perintah, input, dan output sebelum menjalankan perintah apa pun, tanpa membaca file sumber apa pun. Bazel kemudian dapat menyimpan hasil tindakan dalam cache atau mengirim tindakan untuk eksekusi jarak jauh tanpa logika khusus bahasa bawaan.

Memahami label

Label adalah string yang digunakan Bazel untuk mengidentifikasi target atau file. Label digunakan dalam argumen command line dan dalam atribut file BUILD seperti deps. Kita telah melihat beberapa label, seperti //fortune, //:print-fortune, dan @rules_go//go:def.bzl.

Label memiliki tiga bagian: nama repositori, nama paket, dan nama target (atau file).

Nama repositori ditulis di antara @ dan // dan digunakan untuk merujuk ke target dari modul Bazel yang berbeda (karena alasan historis, modul dan repositori terkadang digunakan secara sinonim). Dalam label, @rules_go//go:def.bzl, nama repositorinya adalah rules_go. Nama repositori dapat dihilangkan saat merujuk ke target di repositori yang sama.

Nama paket ditulis di antara // dan : dan digunakan untuk merujuk ke target dari paket Bazel yang berbeda. Dalam label @rules_go//go:def.bzl, nama paketnya adalah go. Paket Bazel adalah sekumpulan file dan target yang ditentukan oleh file BUILD atau BUILD.bazel di direktori tingkat atasnya. Nama paketnya adalah jalur yang dipisahkan dengan garis miring dari direktori utama modul (berisi MODULE.bazel) ke direktori yang berisi file BUILD. Paket dapat menyertakan subdirektori, tetapi hanya jika subdirektori tersebut tidak berisi file BUILD yang menentukan paketnya sendiri.

Sebagian besar project Go memiliki satu file BUILD per direktori dan satu paket Go per file BUILD. Nama paket dalam label dapat dihilangkan saat merujuk ke target di direktori yang sama.

Nama target ditulis setelah : dan merujuk ke target dalam paket. Nama target dapat dihilangkan jika sama dengan komponen terakhir nama paket (sehingga //a/b/c:c sama dengan //a/b/c; //fortune:fortune sama dengan //fortune).

Di command line, Anda dapat menggunakan ... sebagai karakter pengganti untuk merujuk ke semua target dalam paket. Hal ini berguna untuk mem-build atau menguji semua target dalam repositori.

# Build everything
$ bazel build //...

Menguji project

Selanjutnya, pindah ke direktori stage3, tempat kita akan menambahkan pengujian.

go-tutorial/stage3
├── BUILD
├── MODULE.bazel
├── MODULE.bazel.lock
├── fortune
│   ├── BUILD
│   ├── fortune.go
│   └── fortune_test.go
└── print-fortune.go

fortune/fortune_test.go adalah file sumber pengujian baru kita.

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)
    }
}

File ini menggunakan variabel fortunes yang tidak diekspor, sehingga harus dikompilasi ke dalam paket Go yang sama dengan fortune.go. Lihat file BUILD untuk melihat cara kerjanya:

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"],
)

Kita memiliki target fortune_test baru yang menggunakan aturan go_test untuk mengompilasi dan menautkan executable pengujian. go_test perlu mengompilasi fortune.go dan fortune_test.go bersama dengan perintah yang sama, jadi kita menggunakan atribut embed di sini untuk menggabungkan atribut target fortune ke dalam fortune_test. embed paling sering digunakan dengan go_test dan go_binary, tetapi juga berfungsi dengan go_library, yang terkadang berguna untuk kode yang dihasilkan.

Anda mungkin bertanya-tanya apakah atribut embed terkait dengan paket embed Go, yang digunakan untuk mengakses file data yang disalin ke dalam executable. Ini adalah tabrakan nama yang tidak menguntungkan: atribut embed rules_go diperkenalkan sebelum paket embed Go. Sebagai gantinya, rules_go menggunakan embedsrcs untuk mencantumkan file yang dapat dimuat dengan paket embed.

Coba jalankan pengujian kita dengan 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.

Anda dapat menggunakan karakter pengganti ... untuk menjalankan semua pengujian. Bazel juga akan mem-build target yang bukan pengujian, sehingga dapat menangkap error kompilasi bahkan dalam paket yang tidak memiliki pengujian.

$ bazel test //...

Kesimpulan dan bacaan lebih lanjut

Dalam tutorial ini, kita mem-build dan menguji project Go kecil dengan Bazel, dan kita mempelajari beberapa konsep inti Bazel selama proses ini.

  • Untuk mulai mem-build aplikasi lain dengan Bazel, lihat tutorial untuk C++, Java, Android, dan iOS.
  • Anda juga dapat memeriksa daftar aturan yang direkomendasikan untuk bahasa lain.
  • Untuk mengetahui informasi selengkapnya tentang Go, lihat modul rules_go, terutama dokumentasi aturan Go Inti.
  • Untuk mempelajari lebih lanjut cara menggunakan modul Bazel di luar project Anda, lihat dependensi eksternal. Secara khusus, untuk mengetahui informasi tentang cara bergantung pada modul dan toolchain Go melalui sistem modul Bazel, lihat Go dengan bzlmod.