बेज़ल ट्यूटोरियल: एक गो प्रोजेक्ट बनाएं

इस ट्यूटोरियल में, Bazel की बुनियादी बातें बताई गई हैं. इसमें, Go (Golang) प्रोजेक्ट बनाने का तरीका दिखाया गया है. इसमें, आपको अपना वर्कस्पेस सेट अप करने, छोटा प्रोग्राम बनाने, लाइब्रेरी इंपोर्ट करने, और उसका टेस्ट रन करने का तरीका सीखने को मिलेगा. इसके अलावा, आपको Bazel के अहम कॉन्सेप्ट के बारे में भी पता चलेगा. जैसे, टारगेट और BUILD फ़ाइलें.

पूरा होने में लगने वाला अनुमानित समय: 30 मिनट

शुरू करने से पहले

Bazel इंस्टॉल करना

शुरू करने से पहले, अगर आपने Bazel इंस्टॉल नहीं किया है, तो पहले उसे इंस्टॉल करें.

किसी भी डायरेक्ट्री में bazel version चलाकर, यह देखा जा सकता है कि Bazel इंस्टॉल है या नहीं.

Go इंस्टॉल करना (ज़रूरी नहीं)

Bazel की मदद से Go प्रोजेक्ट बनाने के लिए, Go इंस्टॉल करना ज़रूरी नहीं है. Bazel Go के नियम सेट, आपके कंप्यूटर पर इंस्टॉल किए गए टूलचेन का इस्तेमाल करने के बजाय, Go टूलचेन को अपने-आप डाउनलोड और इस्तेमाल करता है. इससे यह पक्का होता है कि किसी प्रोजेक्ट पर काम करने वाले सभी डेवलपर, Go के एक ही वर्शन का इस्तेमाल करके बिल्ड करें.

हालांकि, go get और go mod tidy जैसे कमांड चलाने के लिए, आपको Go टूलचेन इंस्टॉल करना पड़ सकता है.

किसी भी डायरेक्ट्री में go version चलाकर, यह देखा जा सकता है कि Go इंस्टॉल है या नहीं.

सैंपल प्रोजेक्ट पाना

Bazel के उदाहरण, Git डेटाबेस में सेव किए जाते हैं. इसलिए, अगर आपने Git इंस्टॉल नहीं किया है, तो उसे इंस्टॉल करें. उदाहरणों का डेटाबेस डाउनलोड करने के लिए, यह कमांड चलाएं:

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

इस ट्यूटोरियल के लिए सैंपल प्रोजेक्ट, examples/go-tutorial डायरेक्ट्री में है. देखें कि इसमें क्या-क्या शामिल है:

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

इसमें तीन सबडायरेक्ट्री (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 की go.mod फ़ाइल की तरह ही काम करता है. Bazel प्रोजेक्ट में, go.mod फ़ाइल की ज़रूरत नहीं होती. हालांकि, यह फ़ाइल काम की हो सकती है, ताकि डिपेंडेंसी मैनेजमेंट के लिए 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 जनरेट करता है. इसमें हमारी डिपेंडेंसी के हैश और अन्य मेटाडेटा शामिल होते हैं. इसमें Bazel की ओर से जोड़ी गई, इंप्लिसिट डिपेंडेंसी भी शामिल होती हैं. इसलिए, यह काफ़ी लंबी होती है. हम इसे यहां नहीं दिखाएंगे. go.sum की तरह, आपको सोर्स कंट्रोल में अपनी MODULE.bazel.lock फ़ाइल कमिट करनी चाहिए, ताकि यह पक्का किया जा सके कि आपके प्रोजेक्ट पर काम करने वाले सभी लोगों को हर डिपेंडेंसी का एक ही वर्शन मिले. आपको MODULE.bazel.lock को मैन्युअल तरीके से एडिट करने की ज़रूरत नहीं है.

BUILD फ़ाइल को समझना

Bazel के साथ आपका ज़्यादातर इंटरैक्शन, BUILD फ़ाइलों (या BUILD.bazel फ़ाइलों) के ज़रिए होगा. इसलिए, यह समझना ज़रूरी है कि ये फ़ाइलें क्या काम करती हैं.

BUILD फ़ाइलें, Starlark नाम की स्क्रिप्टिंग लैंग्वेज में लिखी जाती हैं. यह Python का सीमित सबसेट है.

एक BUILD फ़ाइल में, टारगेट की सूची शामिल होती है. टारगेट, Bazel की मदद से बिल्ड किया जा सकता है. जैसे, बाइनरी, लाइब्रेरी या टेस्ट.

टारगेट, एट्रिब्यूट की सूची के साथ नियम फ़ंक्शन को कॉल करता है, ताकि यह बताया जा सके कि क्या बिल्ड किया जाना चाहिए. हमारे उदाहरण में दो एट्रिब्यूट हैं: name कमांड लाइन पर टारगेट की पहचान करता है. वहीं, srcs सोर्स फ़ाइल पाथ की सूची है. ये पाथ, BUILD फ़ाइल वाली डायरेक्ट्री के हिसाब से स्लैश से अलग किए जाते हैं.

एक नियम, Bazel को बताता है कि टारगेट को कैसे बिल्ड करना है. हमारे उदाहरण में, हमने go_binary नियम का इस्तेमाल किया है. हर नियम, कार्रवाइयों (कमांड) को तय करता है. ये कार्रवाइयां, आउटपुट फ़ाइलों का सेट जनरेट करती हैं. उदाहरण के लिए, go_binary, Go कंपाइल और लिंक कार्रवाइयों को तय करता है. इनसे, एक्ज़ीक्यूटेबल आउटपुट फ़ाइल बनती है.

Bazel में, Java और C++ जैसी कुछ भाषाओं के लिए बिल्ट-इन नियम होते हैं. इनकी जानकारी, Build Encyclopedia में देखी जा सकती है. 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_binary के बजाय go_library का इस्तेमाल करते हैं.

हमें 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 पैकेज को fortune लाइब्रेरी के importpath एट्रिब्यूट में बताई गई स्ट्रिंग का इस्तेमाल करके इंपोर्ट करता है.

हमें Bazel के लिए, इस डिपेंडेंसी का एलान भी करना होगा. stage2 डायरेक्ट्री में मौजूद BUILD फ़ाइल यहां दी गई है.

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.

लेबल के तीन हिस्से होते हैं: डेटाबेस का नाम, पैकेज का नाम, और टारगेट (या फ़ाइल) का नाम.

डेटाबेस का नाम, @ और // के बीच लिखा जाता है. इसका इस्तेमाल, 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 फ़ाइल और हर BUILD फ़ाइल के लिए Go का एक पैकेज होता है. एक ही डायरेक्ट्री में मौजूद टारगेट को रेफ़र करते समय, लेबल में पैकेज का नाम छोड़ा जा सकता है.

टारगेट का नाम, : के बाद लिखा जाता है. यह किसी पैकेज में मौजूद टारगेट को रेफ़र करता है. टारगेट का नाम तब छोड़ा जा सकता है, जब यह पैकेज के नाम के आखिरी कॉम्पोनेंट जैसा हो . इसलिए, //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 वैरिएबल का इस्तेमाल करती है. इसलिए, इसे fortune.go के साथ, 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 एट्रिब्यूट, Go's embed पैकेज से जुड़ा है. इसका इस्तेमाल, एक्ज़ीक्यूटेबल में कॉपी की गई डेटा फ़ाइलों को ऐक्सेस करने के लिए किया जाता है. यह नाम का टकराव है. rules_go का embed एट्रिब्यूट, Go के embed पैकेज से पहले पेश किया गया था. इसके बजाय, 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 //...

नतीजा और ज़्यादा जानकारी

इस ट्यूटोरियल में, हमने Bazel की मदद से Go के एक छोटे प्रोजेक्ट को बिल्ड और टेस्ट किया. साथ ही, हमने Bazel के कुछ अहम कॉन्सेप्ट के बारे में भी जाना.