Starlark, Python जैसी कॉन्फ़िगरेशन लैंग्वेज है. इसे मूल रूप से Bazel में इस्तेमाल करने के लिए बनाया गया था. बाद में, इसे अन्य टूल में भी इस्तेमाल किया जाने लगा. Bazel की BUILD
और .bzl
फ़ाइलें, Starlark की एक बोली में लिखी जाती हैं. इसे "बिल्ड लैंग्वेज" कहा जाता है. हालांकि, इसे अक्सर "Starlark" कहा जाता है. खास तौर पर, जब यह ज़ोर दिया जाता है कि कोई सुविधा, बिल्ड लैंग्वेज में बताई गई है. इसके बजाय, Bazel का बिल्ट-इन या "नेटिव" हिस्सा है. Bazel, मुख्य भाषा में बिल्ड से जुड़े कई फ़ंक्शन जोड़ता है. जैसे, glob
, genrule
, java_binary
वगैरह.
ज़्यादा जानकारी के लिए, Bazel और Starlark के दस्तावेज़ देखें. साथ ही, नए नियमों के सेट के लिए, Rules SIG टेंप्लेट को शुरुआती पॉइंट के तौर पर इस्तेमाल करें.
खाली नियम
पहला नियम बनाने के लिए, foo.bzl
फ़ाइल बनाएं:
def _foo_binary_impl(ctx):
pass
foo_binary = rule(
implementation = _foo_binary_impl,
)
rule
फ़ंक्शन को कॉल करते समय, आपको कॉलबैक फ़ंक्शन तय करना होगा. लॉजिक वहां जाएगा, लेकिन फ़िलहाल फ़ंक्शन को खाली छोड़ा जा सकता है. ctx
आर्ग्युमेंट, टारगेट के बारे में जानकारी देता है.
नियम को लोड किया जा सकता है और BUILD
फ़ाइल से इस्तेमाल किया जा सकता है.
उसी डायरेक्ट्री में एक BUILD
फ़ाइल बनाएं:
load(":foo.bzl", "foo_binary")
foo_binary(name = "bin")
अब टारगेट बनाया जा सकता है:
$ bazel build bin
INFO: Analyzed target //:bin (2 packages loaded, 17 targets configured).
INFO: Found 1 target...
Target //:bin up-to-date (nothing to build)
भले ही, इस नियम से कोई बदलाव नहीं होता, लेकिन यह अन्य नियमों की तरह ही काम करता है: इसका नाम देना ज़रूरी है. साथ ही, यह visibility
, testonly
, और tags
जैसे सामान्य एट्रिब्यूट के साथ काम करता है.
इवैलुएशन मॉडल
आगे बढ़ने से पहले, यह समझना ज़रूरी है कि कोड का आकलन कैसे किया जाता है.
कुछ प्रिंट स्टेटमेंट के साथ foo.bzl
को अपडेट करें:
def _foo_binary_impl(ctx):
print("analyzing", ctx.label)
foo_binary = rule(
implementation = _foo_binary_impl,
)
print("bzl file evaluation")
और एपीआई बनाना:
load(":foo.bzl", "foo_binary")
print("BUILD file")
foo_binary(name = "bin1")
foo_binary(name = "bin2")
ctx.label
यह उस टारगेट के लेबल से मेल खाता है जिसका विश्लेषण किया जा रहा है. ctx
ऑब्जेक्ट में कई काम के फ़ील्ड और तरीके होते हैं. इनकी पूरी सूची, एपीआई के रेफ़रंस में देखी जा सकती है.
कोड के बारे में क्वेरी करें:
$ bazel query :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
//:bin2
//:bin1
कुछ ऑब्ज़र्वेशन करें:
- "bzl file evaluation" को सबसे पहले प्रिंट किया जाता है. Bazel,
BUILD
फ़ाइल का आकलन करने से पहले, लोड की गई सभी फ़ाइलों का आकलन करता है. अगर कईBUILD
फ़ाइलें foo.bzl लोड कर रही हैं, तो आपको "bzl फ़ाइल का आकलन" सिर्फ़ एक बार दिखेगा. ऐसा इसलिए, क्योंकि Bazel आकलन के नतीजे को कैश मेमोरी में सेव करता है. - कॉलबैक फ़ंक्शन
_foo_binary_impl
को कॉल नहीं किया जाता है. Bazel क्वेरी,BUILD
फ़ाइलें लोड करती है, लेकिन टारगेट का विश्लेषण नहीं करती.
टारगेट का विश्लेषण करने के लिए, cquery
("configured
query") या build
कमांड का इस्तेमाल करें:
$ bazel build :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin1
DEBUG: /usr/home/bazel-codelab/foo.bzl:2:5: analyzing //:bin2
INFO: Analyzed 2 targets (0 packages loaded, 0 targets configured).
INFO: Found 2 targets...
जैसा कि आप देख सकते हैं, _foo_binary_impl
को अब दो बार कॉल किया गया है. हर टारगेट के लिए एक बार.
ध्यान दें कि "bzl फ़ाइल का आकलन" और "BUILD फ़ाइल" को फिर से प्रिंट नहीं किया गया है. ऐसा इसलिए है, क्योंकि foo.bzl
को कॉल करने के बाद, foo.bzl
के आकलन को कैश मेमोरी में सेव कर लिया जाता है.bazel query
Bazel, print
स्टेटमेंट सिर्फ़ तब दिखाता है, जब उन्हें असल में लागू किया जाता है.
फ़ाइल बनाना
अपने नियम को ज़्यादा काम का बनाने के लिए, इसे अपडेट करें, ताकि फ़ाइल जनरेट हो सके. सबसे पहले, फ़ाइल का एलान करें और उसे कोई नाम दें. इस उदाहरण में, टारगेट फ़ाइल के नाम वाली फ़ाइल बनाएं:
ctx.actions.declare_file(ctx.label.name)
bazel build :all
को अभी चलाने पर, आपको यह गड़बड़ी दिखेगी:
The following files have no generating action:
bin2
जब भी किसी फ़ाइल का एलान किया जाता है, तो आपको Bazel को यह बताना होता है कि उसे कैसे जनरेट किया जाए. इसके लिए, आपको एक ऐक्शन बनाना होता है. दिए गए कॉन्टेंट की फ़ाइल बनाने के लिए, ctx.actions.write
का इस्तेमाल करें.
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello\n",
)
कोड मान्य है, लेकिन इससे कुछ नहीं होगा:
$ bazel build bin1
Target //:bin1 up-to-date (nothing to build)
ctx.actions.write
फ़ंक्शन ने एक कार्रवाई रजिस्टर की. इससे Bazel को यह पता चला कि फ़ाइल कैसे जनरेट करनी है. हालांकि, Bazel इस फ़ाइल को तब तक नहीं बनाएगा, जब तक इसके लिए अनुरोध नहीं किया जाता. इसलिए, आखिरी काम यह है कि Bazel को यह बताया जाए कि फ़ाइल, नियम का आउटपुट है. यह नियम को लागू करने के दौरान इस्तेमाल की गई कोई अस्थायी फ़ाइल नहीं है.
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello!\n",
)
return [DefaultInfo(files = depset([out]))]
DefaultInfo
और depset
फ़ंक्शन के बारे में बाद में जानें. फ़िलहाल,
मान लें कि किसी नियम के आउटपुट चुनने का तरीका, आखिरी लाइन है.
अब Bazel चलाएं:
$ bazel build bin1
INFO: Found 1 target...
Target //:bin1 up-to-date:
bazel-bin/bin1
$ cat bazel-bin/bin1
Hello!
आपने फ़ाइल जनरेट कर ली है!
विशेषताएं
नियम को ज़्यादा काम का बनाने के लिए, attr
मॉड्यूल का इस्तेमाल करके नए एट्रिब्यूट जोड़ें और नियम की परिभाषा अपडेट करें.
username
नाम का स्ट्रिंग एट्रिब्यूट जोड़ें:
foo_binary = rule(
implementation = _foo_binary_impl,
attrs = {
"username": attr.string(),
},
)
इसके बाद, इसे BUILD
फ़ाइल में सेट करें:
foo_binary(
name = "bin",
username = "Alice",
)
कॉलबैक फ़ंक्शन में वैल्यू को ऐक्सेस करने के लिए, ctx.attr.username
का इस्तेमाल करें. उदाहरण के लिए:
def _foo_binary_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name)
ctx.actions.write(
output = out,
content = "Hello {}!\n".format(ctx.attr.username),
)
return [DefaultInfo(files = depset([out]))]
ध्यान दें कि आपके पास एट्रिब्यूट को ज़रूरी बनाने या डिफ़ॉल्ट वैल्यू सेट करने का विकल्प होता है. attr.string
के दस्तावेज़ देखें.
आपके पास बूलियन
या पूर्णांकों की सूची जैसे अन्य एट्रिब्यूट टाइप का इस्तेमाल करने का विकल्प भी है.
डिपेंडेंसी
डिपेंडेंसी एट्रिब्यूट, जैसे कि attr.label
और attr.label_list
,
उस टारगेट से डिपेंडेंसी का एलान करते हैं जिसके पास एट्रिब्यूट है. यह एलान उस टारगेट के लिए किया जाता है जिसका लेबल एट्रिब्यूट की वैल्यू में दिखता है. इस तरह का एट्रिब्यूट, टारगेट ग्राफ़ का आधार होता है.
BUILD
फ़ाइल में, टारगेट लेबल एक स्ट्रिंग ऑब्जेक्ट के तौर पर दिखता है. जैसे, //pkg:name
. लागू करने वाले फ़ंक्शन में, टारगेट को Target
ऑब्जेक्ट के तौर पर ऐक्सेस किया जा सकेगा. उदाहरण के लिए, Target.files
का इस्तेमाल करके, टारगेट से मिली फ़ाइलें देखें.
एक से ज़्यादा फ़ाइलें
डिफ़ॉल्ट रूप से, नियमों से बनाए गए टारगेट ही डिपेंडेंसी के तौर पर दिख सकते हैं. जैसे, foo_library()
टारगेट. अगर आपको एट्रिब्यूट के लिए ऐसी फ़ाइलें टारगेट करनी हैं जो इनपुट फ़ाइलें हैं (जैसे, रिपॉज़िटरी में मौजूद सोर्स फ़ाइलें), तो allow_files
का इस्तेमाल करके ऐसा किया जा सकता है. साथ ही, स्वीकार किए गए फ़ाइल एक्सटेंशन की सूची तय की जा सकती है. इसके अलावा, True
का इस्तेमाल करके किसी भी फ़ाइल एक्सटेंशन को अनुमति दी जा सकती है:
"srcs": attr.label_list(allow_files = [".java"]),
फ़ाइलों की सूची को ctx.files.<attribute name>
से ऐक्सेस किया जा सकता है. उदाहरण के लिए, srcs
एट्रिब्यूट में मौजूद फ़ाइलों की सूची को इसके ज़रिए ऐक्सेस किया जा सकता है
ctx.files.srcs
एक फ़ाइल
अगर आपको सिर्फ़ एक फ़ाइल की ज़रूरत है, तो allow_single_file
का इस्तेमाल करें:
"src": attr.label(allow_single_file = [".java"])
इसके बाद, इस फ़ाइल को ctx.file.<attribute name>
में जाकर ऐक्सेस किया जा सकता है:
ctx.file.src
टेंप्लेट का इस्तेमाल करके फ़ाइल बनाना
ऐसा नियम बनाया जा सकता है जो किसी टेंप्लेट के आधार पर .cc फ़ाइल जनरेट करता है. नियम लागू करने वाले फ़ंक्शन में बनाई गई स्ट्रिंग को आउटपुट करने के लिए, ctx.actions.write
का इस्तेमाल भी किया जा सकता है. हालांकि, इसमें दो समस्याएं हैं. पहला, टेंप्लेट बड़ा होने पर, उसे अलग फ़ाइल में रखने से मेमोरी का इस्तेमाल कम होता है. साथ ही, विश्लेषण के दौरान बड़ी स्ट्रिंग बनाने से बचा जा सकता है. दूसरा, अलग फ़ाइल का इस्तेमाल करना उपयोगकर्ता के लिए ज़्यादा आसान होता है. इसके बजाय, ctx.actions.expand_template
का इस्तेमाल करें. यह टेंप्लेट फ़ाइल में बदलाव करता है.
टेंप्लेट फ़ाइल पर निर्भरता का एलान करने के लिए, template
एट्रिब्यूट बनाएं:
def _hello_world_impl(ctx):
out = ctx.actions.declare_file(ctx.label.name + ".cc")
ctx.actions.expand_template(
output = out,
template = ctx.file.template,
substitutions = {"{NAME}": ctx.attr.username},
)
return [DefaultInfo(files = depset([out]))]
hello_world = rule(
implementation = _hello_world_impl,
attrs = {
"username": attr.string(default = "unknown person"),
"template": attr.label(
allow_single_file = [".cc.tpl"],
mandatory = True,
),
},
)
उपयोगकर्ता इस नियम का इस्तेमाल इस तरह कर सकते हैं:
hello_world(
name = "hello",
username = "Alice",
template = "file.cc.tpl",
)
cc_binary(
name = "hello_bin",
srcs = [":hello"],
)
अगर आपको टेंप्लेट को एंड-यूज़र के लिए उपलब्ध नहीं कराना है और हमेशा एक ही टेंप्लेट का इस्तेमाल करना है, तो डिफ़ॉल्ट वैल्यू सेट की जा सकती है. साथ ही, एट्रिब्यूट को निजी बनाया जा सकता है:
"_template": attr.label(
allow_single_file = True,
default = "file.cc.tpl",
),
अंडरस्कोर से शुरू होने वाले एट्रिब्यूट निजी होते हैं. इन्हें BUILD
फ़ाइल में सेट नहीं किया जा सकता. अब टेंप्लेट एक इंप्लिसिट डिपेंडेंसी है: हर hello_world
टारगेट इस फ़ाइल पर निर्भर करता है. BUILD
फ़ाइल को अपडेट करके और exports_files
का इस्तेमाल करके, इस फ़ाइल को अन्य पैकेज के लिए भी उपलब्ध कराना न भूलें:
exports_files(["file.cc.tpl"])
ज़्यादा जानकारी
- नियमों के लिए रेफ़रंस दस्तावेज़ देखें.
- डिपसेट के बारे में जानें.
- उदाहरणों की रिपॉज़िटरी देखें. इसमें नियमों के अन्य उदाहरण भी शामिल हैं.