नियम ट्यूटोरियल

Starlark, Python जैसी कॉन्फ़िगरेशन लैंग्वेज है. इसे मूल रूप से Bazel में इस्तेमाल करने के लिए बनाया गया था. हालांकि, अब इसका इस्तेमाल दूसरे टूल भी करते हैं. Bazel की BUILD और .bzl फ़ाइलें, Starlark की एक बोली में लिखी जाती हैं. इसे "बिल्ड लैंग्वेज" के तौर पर जाना जाता है. हालांकि, इसे अक्सर "Starlark" कहा जाता है. खास तौर पर, जब यह बताना हो कि कोई सुविधा, Bazel के बिल्ट-इन या "नेटिव" पार्ट के बजाय, बिल्ड लैंग्वेज में मौजूद है. Bazel, कोर लैंग्वेज में, बिल्ड से जुड़े कई फ़ंक्शन जोड़ता है. जैसे, glob, genrule, java_binary वगैरह.

ज़्यादा जानकारी के लिए, Bazel और Starlark का दस्तावेज़ देखें. साथ ही, नए नियमसेट के लिए, Rules SIG का टेंप्5}लेट देखें.

खाली नियम

अपना पहला नियम बनाने के लिए, foo.bzl फ़ाइल बनाएं:

def _foo_binary_impl(ctx):
    pass

foo_binary = rule(
    implementation = _foo_binary_impl,
)

जब आप rule फ़ंक्शन को कॉल करते हैं, तो आपको कॉलबैक फ़ंक्शन तय करना होगा. लॉजिक को यहां जोड़ा जाएगा. हालांकि, फ़िलहाल इस फ़ंक्शन को खाली छोड़ा जा सकता है. The 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")

और BUILD को:

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" सबसे पहले प्रिंट होता है. BUILD फ़ाइल का आकलन करने से पहले, Bazel उन सभी फ़ाइलों का आकलन करता है जिन्हें वह लोड करता है. अगर कई BUILD फ़ाइलें foo.bzl को लोड कर रही हैं, तो आपको "bzl file evaluation" सिर्फ़ एक बार दिखेगा. ऐसा इसलिए, क्योंकि Bazel, आकलन के नतीजे को कैश करता है.
  • कॉलबैक फ़ंक्शन _foo_binary_impl को कॉल नहीं किया जाता. Bazel क्वेरी, BUILD फ़ाइलें लोड करती है, लेकिन टारगेट का विश्लेषण नहीं करती.

टारगेट का विश्लेषण करने के लिए, cquery ("कॉन्फ़िगर की गई क्वेरी") या build कमांड का इस्तेमाल करें:

$ bazel build :all
DEBUG: /usr/home/bazel-codelab/foo.bzl:8:1: bzl file evaluation
DEBUG: /usr/home/bazel-codelab/BUILD:2:1: BUILD file
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 file evaluation" फिर से प्रिंट हुआ है. हालांकि, bazel query को कॉल करने के बाद, foo.bzl के आकलन को कैश किया जाता है. Bazel, कोड का फिर से आकलन नहीं करता. यह सिर्फ़ प्रिंट इवेंट को फिर से चलाता है. कैश की स्थिति चाहे जो भी हो, आपको एक जैसा आउटपुट मिलता है.

फ़ाइल बनाना

अपने नियम को ज़्यादा काम का बनाने के लिए, उसे फ़ाइल जनरेट करने के लिए अपडेट करें. सबसे पहले, फ़ाइल की जानकारी दें और उसे कोई नाम दें. इस उदाहरण में, टारगेट के नाम वाली फ़ाइल बनाएं:

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

आगे की जानकारी