บทแนะนำนี้จะแนะนำพื้นฐานของ Bazel โดยแสดงวิธีสร้างโปรเจ็กต์ Go (Golang)
คุณจะได้เรียนรู้วิธีตั้งค่าพื้นที่ทำงาน สร้างโปรแกรมขนาดเล็ก นำเข้าไลบรารี และเรียกใช้การทดสอบ ในระหว่างนี้ คุณจะได้เรียนรู้แนวคิดสำคัญของ Bazel เช่น เป้าหมายและไฟล์ BUILD
เวลาที่ใช้โดยประมาณ: 30 นาที
ก่อนเริ่มต้น
ติดตั้ง Bazel
ก่อนเริ่มต้น ให้ติดตั้ง Bazel ก่อน หากยังไม่ได้ติดตั้ง
คุณตรวจสอบได้ว่าติดตั้ง Bazel แล้วหรือไม่โดยเรียกใช้ bazel version
ในไดเรกทอรีใดก็ได้
ติดตั้ง Go (ไม่บังคับ)
คุณไม่จำเป็นต้องติดตั้ง Go เพื่อสร้างโปรเจ็กต์ Go ด้วย Bazel ชุดกฎ Bazel Go จะดาวน์โหลดและใช้ Toolchain Go โดยอัตโนมัติแทนการใช้ Toolchain ที่ติดตั้งในเครื่อง ซึ่งจะช่วยให้มั่นใจได้ว่า นักพัฒนาซอฟต์แวร์ทุกคนในโปรเจ็กต์จะสร้างด้วย 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 ซึ่งบอกว่าเราต้องการสร้างอะไร
โดยปกติแล้วคุณจะเขียนไฟล์ลักษณะนี้ในแต่ละไดเรกทอรี สำหรับโปรเจ็กต์นี้ เรามีเป้าหมาย 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
ไฟล์ของเรามีการอ้างอิงเดียวใน
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
ไปยังการควบคุมแหล่งที่มาเพื่อ
ให้ทุกคนในโปรเจ็กต์ได้รับทรัพยากร Dependency แต่ละรายการในเวอร์ชันเดียวกัน คุณไม่จำเป็นต้องแก้ไข MODULE.bazel.lock
ด้วยตนเอง
ทำความเข้าใจไฟล์ BUILD
การโต้ตอบส่วนใหญ่กับ Bazel จะผ่านไฟล์ BUILD
(หรือไฟล์ BUILD.bazel
ที่เทียบเท่า) ดังนั้นคุณจึงควรทำความเข้าใจสิ่งที่ไฟล์เหล่านี้ทำ
BUILD
เขียนขึ้นในภาษาการเขียนสคริปต์ที่เรียกว่า Starlark ซึ่งเป็นชุดย่อยที่จำกัดของ Python
ไฟล์ BUILD
มีรายการเป้าหมาย เป้าหมายคือสิ่งที่ Bazel สร้างได้ เช่น ไบนารี ไลบรารี หรือการทดสอบ
เป้าหมายจะเรียกฟังก์ชันกฎพร้อมรายการแอตทริบิวต์เพื่ออธิบายสิ่งที่ควรสร้าง ตัวอย่างของเรามีแอตทริบิวต์ 2 รายการ ได้แก่ name
ซึ่งระบุเป้าหมายในบรรทัดคำสั่ง และ srcs
ซึ่งเป็นรายการเส้นทางไฟล์ต้นทาง (คั่นด้วยเครื่องหมายทับ
เทียบกับไดเรกทอรีที่มีไฟล์ BUILD
)
กฎจะบอก Bazel ว่าจะบิลด์เป้าหมายอย่างไร ในตัวอย่าง เราใช้กฎ
go_binary
แต่ละกฎจะกำหนดการดำเนินการ
(คำสั่ง) ที่สร้างชุดไฟล์เอาต์พุต เช่น go_binary
defines
การดำเนินการคอมไพล์และลิงก์ของ Go ที่สร้างไฟล์เอาต์พุตที่เรียกใช้งานได้
Bazel มีกฎในตัวสำหรับภาษาต่างๆ เช่น Java และ C++ คุณสามารถดูเอกสารประกอบใน Build Encyclopedia คุณดูชุดกฎสำหรับภาษาและเครื่องมืออื่นๆ อีกมากมายได้ในรีจิสทรีกลางของ Bazel (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
เราเห็นมาบ้างแล้ว เช่น //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
หนึ่งไฟล์ต่อไดเรกทอรี และแพ็กเกจ Go หนึ่งแพ็กเกจต่อไฟล์
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
ก่อนแพ็กเกจ embed
ของ Go แต่ rules_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