บทแนะนำนี้จะอธิบายข้อมูลเบื้องต้นเกี่ยวกับ Bazel โดยแสดงวิธีสร้างโปรเจ็กต์ Go (Golang) คุณจะได้เรียนรู้วิธีตั้งค่าเวิร์กスペース สร้างโปรแกรมขนาดเล็ก นําเข้าไลบรารี และเรียกใช้การทดสอบ ในระหว่างนี้ คุณจะได้เรียนรู้แนวคิดหลักของ Bazel เช่น เป้าหมายและไฟล์ BUILD
เวลาโดยประมาณที่ใช้ในการดำเนินการ: 30 นาที
ก่อนเริ่มต้น
ติดตั้ง Bazel
ก่อนเริ่มต้น ให้ติดตั้ง Bazel ก่อน หากยังไม่ได้ทำ
คุณสามารถตรวจสอบว่ามีการติดตั้ง Bazel หรือไม่โดยเรียกใช้ bazel version
ในไดเรกทอรีใดก็ได้
ติดตั้ง Go (ไม่บังคับ)
คุณไม่จําเป็นต้องติดตั้ง Go เพื่อสร้างโปรเจ็กต์ Go ด้วย Bazel ชุดกฎ 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
มีไดเรกทอรีย่อย 3 รายการ (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.mod
ของ Go คุณไม่จำเป็นต้องมีไฟล์ go.mod
ในโปรเจ็กต์ Bazel แต่การมีไฟล์ดังกล่าวอาจมีประโยชน์เพื่อให้คุณใช้ go get
และ go mod tidy
ในการจัดการทรัพยากรต่อไปได้ ชุดกฎ Bazel Go สามารถนําเข้าข้อมูลที่ต้องพึ่งพาจาก go.mod
ได้ แต่เราจะอธิบายเรื่องนี้ในบทแนะนําอื่น
ไฟล์ MODULE.bazel
ของเรามี 1 รายการที่ต้องพึ่งพา นั่นคือ rules_go ซึ่งเป็นชุดกฎ Go เราต้องใช้ข้อกําหนดนี้เนื่องจาก Bazel ไม่มีการสนับสนุน Go ในตัว
bazel_dep(
name = "rules_go",
version = "0.50.1",
)
สุดท้าย MODULE.bazel.lock
คือไฟล์ที่ Bazel สร้างขึ้นซึ่งมีแฮชและข้อมูลเมตาอื่นๆ เกี่ยวกับ Dependency ซึ่งจะรวมข้อกําหนดเบื้องต้นที่ Bazel เพิ่มไว้ด้วย จึงค่อนข้างยาวและเราจะไม่แสดงที่นี่ เช่นเดียวกับ go.sum
คุณควรคอมมิตไฟล์ MODULE.bazel.lock
ไปยังระบบควบคุมแหล่งที่มาเพื่อให้มั่นใจว่าทุกคนในโปรเจ็กต์จะใช้ทรัพยากรแต่ละรายการในเวอร์ชันเดียวกัน คุณไม่จำเป็นต้องแก้ไข MODULE.bazel.lock
ด้วยตนเอง
ทำความเข้าใจไฟล์ BUILD
การโต้ตอบกับ Bazel ส่วนใหญ่จะผ่านไฟล์ BUILD
(หรือไฟล์ BUILD.bazel
) คุณจึงควรทำความเข้าใจสิ่งที่ไฟล์เหล่านี้ทํา
ไฟล์ BUILD
เขียนด้วยภาษาสคริปต์ที่เรียกว่า Starlark ซึ่งเป็นชุดย่อยของ Python
ไฟล์ BUILD
มีรายการเป้าหมาย เป้าหมายคือสิ่งที่ Bazel สามารถสร้างได้ เช่น ไฟล์ไบนารี ไลบรารี หรือทดสอบ
เป้าหมายจะเรียกฟังก์ชันกฎพร้อมรายการแอตทริบิวต์เพื่ออธิบายสิ่งที่ควรสร้าง ตัวอย่างของเรามีแอตทริบิวต์ 2 รายการ ได้แก่ name
ที่ระบุเป้าหมายในบรรทัดคำสั่ง และ srcs
คือรายการเส้นทางไฟล์ต้นทาง (คั่นด้วยเครื่องหมายทับ โดยสัมพันธ์กับไดเรกทอรีที่มีไฟล์ BUILD
)
กฎจะบอก Bazel ว่าจะสร้างเป้าหมายอย่างไร ในตัวอย่างนี้ เราใช้กฎ
go_binary
กฎแต่ละข้อจะกำหนดการดำเนินการ (คําสั่ง) ที่สร้างชุดไฟล์เอาต์พุต เช่น go_binary
กำหนดการดำเนินการคอมไพล์และลิงก์ของ Go ที่จะสร้างไฟล์เอาต์พุตที่เรียกใช้งานได้
Bazel มีกฎในตัวสำหรับภาษาต่างๆ เช่น Java และ C++ คุณสามารถดูเอกสารประกอบในสารานุกรมการสร้าง คุณดูชุดกฎสำหรับภาษาและเครื่องมืออื่นๆ อีกมากมายได้ใน 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
จะนำเข้าแพ็กเกจโดยใช้สตริงเดียวกันกับที่ประกาศไว้ในแอตทริบิวต์ importpath
ของไลบรารี fortune
นอกจากนี้ เรายังต้องประกาศทรัพยากร Dependency นี้กับ Bazel ด้วย นี่คือไฟล์ BUILD
ในไดเรกทอรี
stage2
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
เราเห็นแล้ว 2-3 รายการ เช่น //fortune
,
//:print-fortune
และ @rules_go//go:def.bzl
ป้ายกำกับประกอบด้วย 3 ส่วน ได้แก่ ชื่อที่เก็บ ชื่อแพ็กเกจ และชื่อเป้าหมาย (หรือไฟล์)
ชื่อที่เก็บจะเขียนอยู่ระหว่าง @
และ //
และใช้เพื่ออ้างอิงเป้าหมายจากโมดูล 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
1 ไฟล์ต่อไดเรกทอรีและแพ็กเกจ Go 1 แพ็กเกจต่อไฟล์ BUILD
คุณอาจละชื่อแพ็กเกจในป้ายกำกับได้เมื่ออ้างอิงถึงเป้าหมายในไดเรกทอรีเดียวกัน
ชื่อเป้าหมายจะเขียนต่อจาก :
และหมายถึงเป้าหมายภายในแพ็กเกจ
คุณอาจไม่ต้องระบุชื่อเป้าหมายหากชื่อนั้นเหมือนกับคอมโพเนนต์สุดท้ายของชื่อแพ็กเกจ (ดังนั้น //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
ที่ไม่ได้ส่งออก จึงต้องคอมไพล์เป็นแพ็กเกจ Go เดียวกับ fortune.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
เกี่ยวข้องกับแพ็กเกจ embed
ของ Go หรือไม่ ซึ่งใช้เข้าถึงไฟล์ข้อมูลที่คัดลอกไปยังไฟล์ที่เรียกใช้งานได้ นี่เป็นปัญหาการชนกันของชื่อที่ไม่คาดคิด เนื่องจากแอตทริบิวต์ embed
ของ rules_go เปิดตัวก่อนแพ็กเกจ embed
ของ 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 //...
สรุปและแหล่งข้อมูลเพิ่มเติม
ในบทแนะนํานี้ เราได้สร้างและทดสอบโปรเจ็กต์ Go ขนาดเล็กด้วย Bazel และเรียนรู้แนวคิดหลักๆ ของ Bazel ไปพร้อมๆ กัน
- หากต้องการเริ่มต้นสร้างแอปพลิเคชันอื่นๆ ด้วย Bazel โปรดดูบทแนะนำสำหรับ C++, Java, Android และ iOS
- นอกจากนี้ คุณยังดูรายการกฎที่แนะนำสำหรับภาษาอื่นๆ ได้ด้วย
- ดูข้อมูลเพิ่มเติมเกี่ยวกับ Go ได้ที่ข้อบังคับของข้อบังคับ rules_go โดยเฉพาะเอกสารประกอบกฎ Go หลัก
- ดูข้อมูลเพิ่มเติมเกี่ยวกับการใช้งานโมดูล Bazel นอกโปรเจ็กต์ได้ที่ทรัพยากรภายนอก โดยเฉพาะอย่างยิ่ง ดูข้อมูลเกี่ยวกับวิธีใช้โมดูล Go และชุดเครื่องมือผ่านระบบโมดูลของ Bazel ได้ที่ Go with bzlmod