قوانین

یک قانون مجموعه‌ای از اقدامات را تعریف می‌کند که Bazel روی ورودی‌ها برای تولید مجموعه‌ای از خروجی‌ها انجام می‌دهد، که در ارائه‌دهندگانی که توسط تابع اجرای قانون برگردانده شده‌اند، ارجاع می‌شوند. به عنوان مثال، یک قانون باینری C++ ممکن است:

  1. مجموعه ای از فایل های منبع .cpp . (ورودی ها) را انتخاب کنید.
  2. g++ را روی فایل های منبع اجرا کنید (عمل).
  3. ارائه‌دهنده DefaultInfo را با خروجی‌های اجرایی و فایل‌های دیگر برای در دسترس قرار دادن در زمان اجرا برگردانید.
  4. ارائه دهنده CcInfo را با اطلاعات اختصاصی C++ که از هدف و وابستگی های آن جمع آوری شده است، برگردانید.

از دیدگاه بازل، g++ و کتابخانه های استاندارد C++ نیز ورودی های این قانون هستند. به عنوان یک قانون نویس، شما باید نه تنها ورودی های ارائه شده توسط کاربر را در نظر بگیرید، بلکه باید تمام ابزارها و کتابخانه های مورد نیاز برای اجرای اقدامات را نیز در نظر بگیرید.

قبل از ایجاد یا تغییر هر قانون، مطمئن شوید که با مراحل ساخت Bazel آشنا هستید. درک سه مرحله ساخت (بارگذاری، تجزیه و تحلیل و اجرا) مهم است. همچنین یادگیری در مورد ماکروها برای درک تفاوت بین قوانین و ماکروها مفید است. برای شروع، ابتدا آموزش قوانین را مرور کنید. سپس، از این صفحه به عنوان مرجع استفاده کنید.

چند قانون در خود Bazel تعبیه شده است. این قوانین بومی ، مانند cc_library و java_binary ، برخی از زبان‌های خاص را پشتیبانی می‌کنند. با تعریف قوانین خود، می توانید پشتیبانی مشابهی را برای زبان ها و ابزارهایی که Bazel به صورت بومی پشتیبانی نمی کند، اضافه کنید.

Bazel یک مدل توسعه پذیری برای نوشتن قوانین با استفاده از زبان Starlark ارائه می دهد. این قوانین در فایل‌های .bzl . نوشته شده‌اند که می‌توانند مستقیماً از فایل‌های BUILD بارگیری شوند.

هنگام تعریف قانون خود، باید تصمیم بگیرید که از چه ویژگی هایی پشتیبانی می کند و چگونه خروجی های خود را تولید می کند.

تابع implementation قانون رفتار دقیق آن را در مرحله تجزیه و تحلیل تعریف می کند. این تابع هیچ فرمان خارجی را اجرا نمی کند. در عوض، اقداماتی را ثبت می‌کند که بعداً در مرحله اجرا برای ساختن خروجی‌های قانون، در صورت نیاز، استفاده می‌شوند.

ایجاد قانون

در یک فایل .bzl . از تابع rule برای تعریف یک قانون جدید استفاده کنید و نتیجه را در یک متغیر سراسری ذخیره کنید. فراخوانی rule ویژگی ها و یک تابع پیاده سازی را مشخص می کند:

example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        "deps": attr.label_list(),
        ...
    },
)

این یک نوع قانون به نام example_library را تعریف می کند.

فراخوانی به rule همچنین باید مشخص کند که آیا قانون یک خروجی اجرایی (با executable=True )، یا به طور خاص یک آزمایشی اجرایی (با test=True ) ایجاد می کند. اگر دومی باشد، قانون یک قانون آزمایشی است و نام قانون باید به _test شود.

نمونه سازی هدف

قوانین را می توان در فایل های BUILD بارگیری و فراخوانی کرد:

load('//some/pkg:rules.bzl', 'example_library')

example_library(
    name = "example_target",
    deps = [":another_target"],
    ...
)

هر فراخوانی به یک قانون ساخت، هیچ مقداری را برمی‌گرداند، اما اثر جانبی تعریف یک هدف را دارد. به این می گویند مصداق قاعده. این یک نام برای هدف جدید و مقادیر برای ویژگی های هدف مشخص می کند.

قوانین همچنین می توانند از توابع Starlark فراخوانی شوند و در فایل های .bzl . بارگذاری شوند. توابع Starlark که قوانین را فراخوانی می کنند ماکروهای Starlark نامیده می شوند. ماکروهای Starlark در نهایت باید از فایل‌های BUILD فراخوانی شوند و فقط در مرحله بارگیری ، زمانی که فایل‌های BUILD برای نمونه‌سازی اهداف ارزیابی می‌شوند، فراخوانی می‌شوند.

ویژگی های

یک صفت یک استدلال قاعده است. ویژگی‌ها می‌توانند مقادیر خاصی را برای پیاده‌سازی یک هدف ارائه کنند، یا می‌توانند به اهداف دیگر رجوع کنند و نموداری از وابستگی‌ها ایجاد کنند.

ویژگی های خاص قانون، مانند srcs یا deps ، با ارسال یک نقشه از نام ویژگی ها به طرحواره ها (ایجاد شده با استفاده از ماژول attr ) به پارامتر attrs rule تعریف می شوند. ویژگی‌های رایج ، مانند name و visibility بودن، به طور ضمنی به همه قوانین اضافه می‌شوند. ویژگی های اضافی به طور ضمنی به قوانین اجرایی و آزمایشی به طور خاص اضافه می شوند. ویژگی هایی که به طور ضمنی به یک قانون اضافه می شوند را نمی توان در فرهنگ لغت ارسال شده به attrs .

ویژگی های وابستگی

قوانینی که کد منبع را پردازش می کنند معمولاً ویژگی های زیر را برای مدیریت انواع وابستگی ها تعریف می کنند:

  • srcs فایل های منبع پردازش شده توسط اقدامات هدف را مشخص می کند. اغلب، طرح ویژگی مشخص می کند که کدام پسوند فایل برای نوع فایل منبع فرآیندهای قانون مورد انتظار است. قوانین برای زبان‌های دارای فایل‌های هدر معمولاً یک ویژگی hdrs جداگانه برای سرصفحه‌های پردازش شده توسط یک هدف و مصرف‌کنندگان آن مشخص می‌کنند.
  • deps وابستگی کد را برای یک هدف مشخص می کند. طرح ویژگی باید مشخص کند که این وابستگی ها باید کدام ارائه دهندگان را ارائه دهند. (به عنوان مثال، cc_library CcInfo ارائه می دهد.)
  • data فایل هایی را مشخص می کند که در زمان اجرا برای هر فایل اجرایی که به هدف بستگی دارد در دسترس قرار گیرد. که باید اجازه دهد فایل های دلخواه مشخص شوند.
example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        "srcs": attr.label_list(allow_files = [".example"]),
        "hdrs": attr.label_list(allow_files = [".header"]),
        "deps": attr.label_list(providers = [ExampleInfo]),
        "data": attr.label_list(allow_files = True),
        ...
    },
)

اینها نمونه هایی از ویژگی های وابستگی هستند . هر ویژگی که یک برچسب ورودی را مشخص می‌کند (آنهایی که با attr.label_list ، attr.label ، یا attr.label_keyed_string_dict ) وابستگی‌هایی از نوع خاصی را بین یک هدف و اهدافی مشخص می‌کند که برچسب‌ها (یا اشیاء Label مربوطه) در آن ویژگی فهرست شده‌اند. وقتی هدف تعریف می شود مخزن، و احتمالاً مسیر، برای این برچسب ها نسبت به هدف تعریف شده حل می شود.

example_library(
    name = "my_target",
    deps = [":other_target"],
)

example_library(
    name = "other_target",
    ...
)

در این مثال، other_target یک وابستگی به my_target است، و بنابراین other_target ابتدا تجزیه و تحلیل می شود. اگر یک چرخه در نمودار وابستگی اهداف وجود داشته باشد، خطا است.

ویژگی های خصوصی و وابستگی های ضمنی

یک ویژگی وابستگی با مقدار پیش‌فرض یک وابستگی ضمنی ایجاد می‌کند. این امر ضمنی است زیرا بخشی از نمودار هدف است که کاربر در فایل BUILD آن را مشخص نمی کند. وابستگی های ضمنی برای کدگذاری سخت رابطه بین یک قانون و یک ابزار (یک وابستگی در زمان ساخت، مانند یک کامپایلر) مفید هستند، زیرا اغلب اوقات کاربر علاقه ای به تعیین ابزاری که قانون استفاده می کند، ندارد. در داخل تابع اجرای قانون، این مورد همانند سایر وابستگی ها در نظر گرفته می شود.

اگر می‌خواهید یک وابستگی ضمنی ارائه دهید بدون اینکه به کاربر اجازه دهید آن مقدار را لغو کند، می‌توانید ویژگی را با دادن نامی که با زیرخط ( _ ) شروع می‌شود خصوصی کنید. ویژگی های خصوصی باید دارای مقادیر پیش فرض باشند. به طور کلی فقط استفاده از ویژگی های خصوصی برای وابستگی های ضمنی منطقی است.

example_library = rule(
    implementation = _example_library_impl,
    attrs = {
        ...
        "_compiler": attr.label(
            default = Label("//tools:example_compiler"),
            allow_single_file = True,
            executable = True,
            cfg = "exec",
        ),
    },
)

در این مثال، هر هدف از نوع example_library یک وابستگی ضمنی به کامپایلر //tools:example_compiler دارد. این به تابع پیاده سازی example_library اجازه می دهد تا اقداماتی را ایجاد کند که کامپایلر را فراخوانی می کند، حتی اگر کاربر برچسب آن را به عنوان ورودی ارسال نکرده باشد. از آنجایی که _compiler یک ویژگی خصوصی است، نتیجه می شود که ctx.attr._compiler همیشه در تمام اهداف این نوع قانون به //tools:example_compiler اشاره می کند. همچنین، می‌توانید compiler ویژگی را بدون خط زیر نامگذاری کنید و مقدار پیش‌فرض را حفظ کنید. این به کاربران اجازه می دهد در صورت لزوم کامپایلر دیگری را جایگزین کنند، اما نیازی به آگاهی از برچسب کامپایلر ندارد.

وابستگی های ضمنی معمولاً برای ابزارهایی استفاده می شود که در همان مخزن اجرای قانون قرار دارند. اگر ابزار از پلتفرم اجرا یا مخزن دیگری می آید، قانون باید آن ابزار را از زنجیره ابزار بدست آورد.

ویژگی های خروجی

ویژگی های خروجی ، مانند attr.output و attr.output_list ، یک فایل خروجی را که هدف تولید می کند، اعلام می کند. اینها از دو جهت با ویژگی های وابستگی متفاوت هستند:

  • آنها به جای ارجاع به اهداف تعریف شده در جای دیگر، اهداف فایل خروجی را تعریف می کنند.
  • اهداف فایل خروجی به جای برعکس، به هدف قانون نمونه سازی شده بستگی دارد.

به طور معمول، ویژگی های خروجی تنها زمانی استفاده می شوند که یک قانون نیاز به ایجاد خروجی با نام های تعریف شده توسط کاربر داشته باشد که نمی تواند بر اساس نام هدف باشد. اگر یک قانون دارای یک ویژگی خروجی باشد، معمولاً نامگذاری out یا outs .

ویژگی های خروجی روش ترجیحی برای ایجاد خروجی های از پیش اعلام شده است که می تواند به طور خاص به آن وابسته باشد یا در خط فرمان درخواست شود .

تابع پیاده سازی

هر قانون به یک تابع implementation نیاز دارد. این توابع به شدت در مرحله تجزیه و تحلیل اجرا می شوند و نمودار اهداف تولید شده در مرحله بارگذاری را به نموداری از اقداماتی که در مرحله اجرا انجام می شود تبدیل می کنند. به این ترتیب، توابع پیاده سازی در واقع نمی توانند فایل ها را بخوانند یا بنویسند.

توابع اجرای قانون معمولاً خصوصی هستند (با علامت زیر خط اصلی نامگذاری می شوند). به طور متعارف، آنها به عنوان قانون خود نامگذاری می شوند، اما پسوند _impl هستند.

توابع پیاده‌سازی دقیقاً یک پارامتر را می‌گیرند: یک زمینه قانون که معمولاً ctx نامیده می‌شود. آنها لیستی از ارائه دهندگان را برمی گردانند.

اهداف

وابستگی ها در زمان تجزیه و تحلیل به عنوان اشیاء Target نشان داده می شوند. این اشیاء حاوی ارائه دهندگانی هستند که هنگام اجرای تابع پیاده سازی هدف ایجاد شده اند.

ctx.attr دارای فیلدهای مربوط به نام هر ویژگی وابستگی است که شامل اشیاء Target است که هر وابستگی مستقیم را از طریق آن ویژگی نشان می دهد. برای ویژگی‌های label_list ، این فهرستی از Targets است. برای ویژگی‌های label ، این یک Target یا None است.

لیستی از اشیاء ارائه دهنده توسط تابع پیاده سازی یک هدف برگردانده می شود:

return [ExampleInfo(headers = depset(...))]

آنها را می توان با استفاده از نماد شاخص ( [] ) با نوع ارائه دهنده به عنوان یک کلید دسترسی داشت. اینها می توانند ارائه دهندگان سفارشی تعریف شده در Starlark یا ارائه دهندگان قوانین بومی موجود به عنوان متغیرهای جهانی Starlark باشند.

به عنوان مثال، اگر یک قانون، فایل‌های هدر را از طریق یک ویژگی hdrs و آنها را در اختیار اقدامات کامپایل‌سازی هدف و مصرف‌کنندگانش قرار دهد، می‌تواند آنها را به این صورت جمع‌آوری کند:

def _example_library_impl(ctx):
    ...
    transitive_headers = [hdr[ExampleInfo].headers for hdr in ctx.attr.hdrs]

برای سبک قدیمی که در آن یک struct به جای لیستی از اشیاء ارائه‌دهنده، از تابع پیاده‌سازی هدف بازگردانده می‌شود:

return struct(example_info = struct(headers = depset(...)))

ارائه دهندگان را می توان از فیلد مربوطه شی Target بازیابی کرد:

transitive_headers = [hdr.example_info.headers for hdr in ctx.attr.hdrs]

این سبک به شدت ممنوع است و قوانین باید از آن حذف شوند.

فایل ها

فایل ها با اشیاء File نشان داده می شوند. از آنجایی که Bazel ورودی/خروجی فایل را در مرحله تجزیه و تحلیل انجام نمی دهد، از این اشیاء نمی توان مستقیماً برای خواندن یا نوشتن محتوای فایل استفاده کرد. در عوض، آنها به توابع ارسال کنش (به ctx.actions مراجعه کنید) منتقل می شوند تا قطعاتی از گراف عمل بسازند.

یک File می تواند یک فایل منبع یا یک فایل تولید شده باشد. هر فایل تولید شده باید دقیقاً خروجی یک عمل باشد. فایل های منبع نمی توانند خروجی هر عملی باشند.

برای هر ویژگی وابستگی، فیلد مربوطه ctx.files حاوی لیستی از خروجی های پیش فرض همه وابستگی ها از طریق آن ویژگی است:

def _example_library_impl(ctx):
    ...
    headers = depset(ctx.files.hdrs, transitive=transitive_headers)
    srcs = ctx.files.srcs
    ...

ctx.file حاوی یک File یا None برای ویژگی‌های وابستگی است که مشخصات آن تنظیم شده است allow_single_file=True . ctx.executable مانند ctx.file عمل می‌کند، اما فقط حاوی فیلدهایی برای ویژگی‌های وابستگی است که مشخصات آن executable=True تنظیم شده است.

اعلام خروجی ها

در طول مرحله تجزیه و تحلیل، تابع اجرای یک قانون می تواند خروجی ایجاد کند. از آنجایی که تمام برچسب ها باید در مرحله بارگذاری شناخته شوند، این خروجی های اضافی هیچ برچسبی ندارند. اشیاء File برای خروجی ها را می توان با استفاده از ctx.actions.declare_file و ctx.actions.declare_directory . اغلب، نام خروجی ها بر اساس نام هدف، ctx.label.name :

def _example_library_impl(ctx):
  ...
  output_file = ctx.actions.declare_file(ctx.label.name + ".output")
  ...

برای خروجی های از پیش اعلام شده، مانند خروجی های ایجاد شده برای ویژگی های خروجی ، اشیاء File را می توان از فیلدهای مربوطه ctx.outputs کرد.

اقدامات

یک عمل نحوه تولید مجموعه‌ای از خروجی‌ها از مجموعه‌ای از ورودی‌ها را توضیح می‌دهد، برای مثال «gcc را در hello.c اجرا کنید و hello.o را دریافت کنید». هنگامی که یک عمل ایجاد می شود، Bazel بلافاصله دستور را اجرا نمی کند. آن را در نموداری از وابستگی ها ثبت می کند، زیرا یک عمل می تواند به خروجی یک عمل دیگر بستگی داشته باشد. به عنوان مثال، در C، پیوند دهنده باید پس از کامپایلر فراخوانی شود.

توابع همه منظوره که اکشن ها را ایجاد می کنند در ctx.actions تعریف شده اند:

ctx.actions.args می تواند برای جمع آوری کارآمد آرگومان های اعمال استفاده شود. تا زمان اجرا از صاف کردن عمق ها جلوگیری می کند:

def _example_library_impl(ctx):
    ...

    transitive_headers = [dep[ExampleInfo].headers for dep in ctx.attr.deps]
    headers = depset(ctx.files.hdrs, transitive=transitive_headers)
    srcs = ctx.files.srcs
    inputs = depset(srcs, transitive=[headers])
    output_file = ctx.actions.declare_file(ctx.label.name + ".output")

    args = ctx.actions.args()
    args.add_joined("-h", headers, join_with=",")
    args.add_joined("-s", srcs, join_with=",")
    args.add("-o", output_file)

    ctx.actions.run(
        mnemonic = "ExampleCompile",
        executable = ctx.executable._compiler,
        arguments = [args],
        inputs = inputs,
        outputs = [output_file],
    )
    ...

کنش‌ها فهرست یا depset از فایل‌های ورودی را می‌گیرند و فهرستی (غیر خالی) از فایل‌های خروجی ایجاد می‌کنند. مجموعه فایل های ورودی و خروجی باید در مرحله تجزیه و تحلیل شناخته شوند. ممکن است به مقدار ویژگی ها، از جمله ارائه دهندگان وابستگی ها، بستگی داشته باشد، اما نمی تواند به نتیجه اجرا بستگی داشته باشد. به عنوان مثال، اگر اکشن شما دستور unzip را اجرا می کند، باید مشخص کنید که انتظار دارید کدام فایل ها باد شوند (قبل از اجرای unzip). اقداماتی که تعداد متغیری از فایل‌ها را در داخل ایجاد می‌کنند، می‌توانند آن‌ها را در یک فایل (مانند یک فایل فشرده، تار، یا فرمت بایگانی دیگر) قرار دهند.

اقدامات باید همه ورودی های خود را فهرست کنند. فهرست کردن ورودی هایی که استفاده نمی شوند مجاز است، اما ناکارآمد است.

اقدامات باید همه خروجی های خود را ایجاد کنند. آنها ممکن است فایل های دیگری بنویسند، اما هر چیزی که در خروجی نباشد در دسترس مصرف کنندگان نخواهد بود. تمام خروجی های اعلام شده باید با یک عمل نوشته شوند.

عملکردها با توابع خالص قابل مقایسه هستند: آنها باید فقط به ورودی های ارائه شده بستگی داشته باشند و از دسترسی به اطلاعات رایانه، نام کاربری، ساعت، شبکه یا دستگاه های ورودی/خروجی (به جز خواندن ورودی ها و خروجی های نوشتن) خودداری کنند. این مهم است زیرا خروجی در حافظه پنهان ذخیره می شود و مجددا استفاده می شود.

وابستگی ها توسط Bazel حل می شود، که تصمیم می گیرد کدام اقدامات اجرا شوند. اگر یک چرخه در نمودار وابستگی وجود داشته باشد، خطا است. ایجاد یک عمل تضمینی برای اجرا شدن آن نیست، که بستگی به این دارد که آیا خروجی های آن برای ساخت مورد نیاز است یا خیر.

ارائه دهندگان

ارائه دهندگان اطلاعاتی هستند که یک قانون در معرض سایر قوانین وابسته به آن قرار می دهد. این داده‌ها می‌تواند شامل فایل‌های خروجی، کتابخانه‌ها، پارامترهایی برای ارسال در خط فرمان ابزار یا هر چیز دیگری باشد که مصرف‌کنندگان هدف باید درباره آن بدانند.

از آنجایی که عملکرد اجرای یک قانون فقط می‌تواند ارائه‌دهندگان را از وابستگی‌های فوری هدف نمونه‌سازی شده بخواند، قوانین باید هر اطلاعاتی را از وابستگی‌های یک هدف که باید توسط مصرف‌کنندگان هدف شناخته شوند، ارسال کنند، معمولاً با انباشت آن در یک depset .

ارائه دهندگان یک هدف با لیستی از اشیاء Provider که توسط تابع پیاده سازی برگردانده شده اند، مشخص می شوند.

توابع پیاده سازی قدیمی را نیز می توان به سبک قدیمی نوشت که در آن تابع پیاده سازی به جای لیست اشیاء ارائه دهنده، یک struct برمی گرداند. این سبک به شدت ممنوع است و قوانین باید از آن حذف شوند.

خروجی های پیش فرض

خروجی‌های پیش‌فرض یک هدف، خروجی‌هایی هستند که به‌طور پیش‌فرض زمانی که هدف برای ساخت در خط فرمان درخواست می‌شود، درخواست می‌شوند. به عنوان مثال، یک هدف java_library //pkg:foo دارای foo.jar به عنوان خروجی پیش‌فرض است، بنابراین با دستور bazel build //pkg:foo ساخته می‌شود.

خروجی های پیش فرض توسط پارامتر files DefaultInfo مشخص می شود:

def _example_library_impl(ctx):
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        ...
    ]

اگر DefaultInfo توسط اجرای قانون برگردانده نشود یا پارامتر files ها مشخص نشده باشد، DefaultInfo.files به طور پیش فرض برای همه خروجی های از پیش اعلام شده (به طور کلی، خروجی هایی که توسط ویژگی های خروجی ایجاد می شوند) پیش فرض می شود.

قوانینی که اقدامات را انجام می دهند باید خروجی های پیش فرض را ارائه دهند، حتی اگر انتظار نمی رود که این خروجی ها مستقیماً مورد استفاده قرار گیرند. اقداماتی که در نمودار خروجی های درخواستی وجود ندارند، هرس می شوند. اگر یک خروجی فقط توسط مصرف کنندگان هدف مورد استفاده قرار گیرد، زمانی که هدف به صورت مجزا ساخته شده باشد، آن اقدامات انجام نمی شود. این امر اشکال‌زدایی را دشوارتر می‌کند، زیرا بازسازی فقط هدف ناموفق، شکست را بازتولید نمی‌کند.

فایل های اجرا شده

Runfiles مجموعه ای از فایل ها هستند که توسط یک هدف در زمان اجرا (بر خلاف زمان ساخت) مورد استفاده قرار می گیرند. در طول مرحله اجرا ، Bazel یک درخت دایرکتوری ایجاد می کند که حاوی پیوندهای نمادین است که به فایل های اجرائی اشاره می کنند. این محیط را برای باینری مرحله بندی می کند تا بتواند در طول زمان اجرا به فایل های اجرا دسترسی داشته باشد.

Runfiles را می توان به صورت دستی در حین ایجاد قانون اضافه کرد. اشیاء runfiles را می توان با روش runfiles در زمینه قانون، ctx.runfiles کرد و به پارامتر runfiles در DefaultInfo ارسال کرد. خروجی اجرایی قوانین اجرایی به طور ضمنی به فایل های اجرا اضافه می شود.

برخی از قوانین ویژگی هایی را مشخص می کنند که معمولاً data نامیده می شوند و خروجی های آنها به فایل های اجرا شده هدف اضافه می شود. Runfiles همچنین باید از data و همچنین از هر ویژگی که ممکن است کدی را برای اجرای نهایی ارائه دهد، به طور کلی srcs (که ممکن است حاوی اهداف گروه filegroup با data ) و deps ادغام شوند.

def _example_library_impl(ctx):
    ...
    runfiles = ctx.runfiles(files = ctx.files.data)
    transitive_runfiles = []
    for runfiles_attr in (
        ctx.attr.srcs,
        ctx.attr.hdrs,
        ctx.attr.deps,
        ctx.attr.data,
    ):
        for target in runfiles_attr:
            transitive_runfiles.append(target[DefaultInfo].default_runfiles)
    runfiles = runfiles.merge_all(transitive_runfiles)
    return [
        DefaultInfo(..., runfiles = runfiles),
        ...
    ]

ارائه دهندگان سفارشی

ارائه دهندگان را می توان با استفاده از تابع provider برای انتقال اطلاعات خاص قانون تعریف کرد:

ExampleInfo = provider(
    "Info needed to compile/link Example code.",
    fields={
        "headers": "depset of header Files from transitive dependencies.",
        "files_to_link": "depset of Files from compilation.",
    })

سپس توابع پیاده سازی قانون می توانند نمونه های ارائه دهنده را بسازند و برگردانند:

def _example_library_impl(ctx):
  ...
  return [
      ...
      ExampleInfo(
          headers = headers,
          files_to_link = depset(
              [output_file],
              transitive = [
                  dep[ExampleInfo].files_to_link for dep in ctx.attr.deps
              ],
          ),
      )
  ]
مقداردهی اولیه ارائه دهندگان سفارشی

این امکان وجود دارد که از نمونه یک ارائه دهنده با پیش پردازش سفارشی و منطق اعتبار سنجی محافظت کنید. این می‌تواند برای اطمینان از اینکه همه نمونه‌های ارائه‌دهنده از متغیرهای ثابت اطاعت می‌کنند، یا به کاربران یک API تمیزتر برای به دست آوردن یک نمونه، استفاده می‌شود.

این کار با ارسال یک تماس init به تابع provider انجام می شود. اگر این فراخوانی داده شود، نوع برگشتی provider() به دو مقدار تبدیل می‌شود: نماد ارائه‌دهنده که مقدار بازگشتی معمولی زمانی که init استفاده نمی‌شود، و یک "سازنده خام".

در این حالت، زمانی که نماد ارائه‌دهنده فراخوانی می‌شود، به‌جای اینکه مستقیماً یک نمونه جدید را برگرداند، آرگومان‌ها را در امتداد init اولیه ارسال می‌کند. مقدار برگشتی فراخوان باید نگاشت نام فیلدها (رشته ها) به مقادیر باشد. این برای مقداردهی اولیه فیلدهای نمونه جدید استفاده می شود. توجه داشته باشید که callback ممکن است هر گونه امضایی داشته باشد و اگر آرگومان ها با امضا مطابقت نداشته باشند، خطایی گزارش می شود که گویی پاسخ تماس مستقیماً فراخوانی شده است.

برعکس سازنده خام، فراخوان اولیه را دور می init .

مثال زیر از init برای پیش پردازش و تایید آرگومان های آن استفاده می کند:

# //pkg:exampleinfo.bzl

_core_headers = [...]  # private constant representing standard library files

# It's possible to define an init accepting positional arguments, but
# keyword-only arguments are preferred.
def _exampleinfo_init(*, files_to_link, headers = None, allow_empty_files_to_link = False):
    if not files_to_link and not allow_empty_files_to_link:
        fail("files_to_link may not be empty")
    all_headers = depset(_core_headers, transitive = headers)
    return {'files_to_link': files_to_link, 'headers': all_headers}

ExampleInfo, _new_exampleinfo = provider(
    ...
    init = _exampleinfo_init)

export ExampleInfo

سپس اجرای یک قانون ممکن است ارائه دهنده را به صورت زیر نمونه سازی کند:

    ExampleInfo(
        files_to_link=my_files_to_link,  # may not be empty
        headers = my_headers,  # will automatically include the core headers
    )

سازنده خام می تواند برای تعریف توابع کارخانه عمومی جایگزین که از منطق init عبور نمی کنند استفاده شود. به عنوان مثال، در exampleinfo.bzl می توانیم تعریف کنیم:

def make_barebones_exampleinfo(headers):
    """Returns an ExampleInfo with no files_to_link and only the specified headers."""
    return _new_exampleinfo(files_to_link = depset(), headers = all_headers)

به طور معمول، سازنده خام به متغیری که نام آن با زیرخط ( _new_exampleinfo در بالا) شروع می شود، مقید است، به طوری که کد کاربر نمی تواند آن را بارگیری کند و نمونه های ارائه دهنده دلخواه را ایجاد کند.

یکی دیگر از کاربردهای init این است که به سادگی مانع از فراخوانی نماد ارائه‌دهنده توسط کاربر و مجبور کردن او به استفاده از یک تابع کارخانه به جای آن می‌شود:

def _exampleinfo_init_banned(*args, **kwargs):
    fail("Do not call ExampleInfo(). Use make_exampleinfo() instead.")

ExampleInfo, _new_exampleinfo = provider(
    ...
    init = _exampleinfo_init_banned)

def make_exampleinfo(...):
    ...
    return _new_exampleinfo(...)

قوانین اجرایی و قوانین تست

قوانین اجرایی، اهدافی را تعریف می‌کنند که می‌توانند توسط دستور bazel run فراخوانی شوند. قوانین تست نوع خاصی از قوانین اجرایی هستند که اهداف آن را می توان با دستور bazel test نیز فراخوانی کرد. قوانین اجرایی و آزمایشی با تنظیم آرگومان executable یا test مربوطه به True در فراخوانی rule ایجاد می‌شوند:

example_binary = rule(
   implementation = _example_binary_impl,
   executable = True,
   ...
)

example_test = rule(
   implementation = _example_binary_impl,
   test = True,
   ...
)

قوانین آزمون باید دارای نام هایی باشد که به _test می شوند. (اسامی هدف های آزمایشی نیز اغلب بر اساس قرارداد به _test می شوند، اما این الزامی نیست.) قوانین غیر آزمون نباید دارای این پسوند باشند.

هر دو نوع قانون باید یک فایل خروجی اجرایی تولید کنند (که ممکن است از قبل اعلام شده باشد یا نباشد) که توسط دستورات run یا test فراخوانی می شود. برای اینکه به Bazel بگویید از کدام یک از خروجی های یک قانون به عنوان این فایل اجرایی استفاده کند، آن را به عنوان آرگومان executable ارائه دهنده DefaultInfo برگشتی ارسال کنید. آن executable به خروجی‌های پیش‌فرض قانون اضافه می‌شود (بنابراین نیازی نیست آن را به executable و files ارسال کنید). همچنین به طور ضمنی به فایل های اجرا شده اضافه شده است:

def _example_binary_impl(ctx):
    executable = ctx.actions.declare_file(ctx.label.name)
    ...
    return [
        DefaultInfo(executable = executable, ...),
        ...
    ]

اقدامی که این فایل را تولید می کند باید بیت اجرایی را روی فایل تنظیم کند. برای یک ctx.actions.run یا ctx.actions.run_shell ، این کار باید توسط ابزار زیربنایی که توسط اکشن فراخوانی می شود انجام شود. برای یک عمل ctx.actions.write ، پاس is_executable=True .

به عنوان رفتار قدیمی ، قوانین اجرایی دارای یک خروجی از پیش ctx.outputs.executable هستند. اگر با استفاده از DefaultInfo یکی را مشخص نکنید، این فایل به عنوان فایل اجرایی پیش‌فرض عمل می‌کند. در غیر این صورت نباید از آن استفاده کرد. این مکانیزم خروجی منسوخ شده است زیرا از سفارشی کردن نام فایل اجرایی در زمان تجزیه و تحلیل پشتیبانی نمی کند.

نمونه هایی از یک قانون اجرایی و یک قانون تست را ببینید.

قواعد اجرایی و قوانین تست دارای ویژگی‌های اضافی هستند که به طور ضمنی تعریف شده‌اند، علاوه بر مواردی که برای همه قوانین اضافه شده‌اند. پیش‌فرض‌های ویژگی‌های اضافه‌شده ضمنی را نمی‌توان تغییر داد، اگرچه می‌توان با قرار دادن یک قانون خصوصی در یک ماکرو Starlark که پیش‌فرض را تغییر می‌دهد، این کار را انجام داد:

def example_test(size="small", **kwargs):
  _example_test(size=size, **kwargs)

_example_test = rule(
 ...
)

محل اجرای فایل ها

هنگامی که یک هدف اجرایی با اجرای bazel run (یا test ) اجرا می شود، ریشه دایرکتوری runfiles در مجاورت فایل اجرایی قرار می گیرد. مسیرها به شرح زیر است:

# Given executable_file and runfile_file:
runfiles_root = executable_file.path + ".runfiles"
workspace_name = ctx.workspace_name
runfile_path = runfile_file.short_path
execution_root_relative_path = "%s/%s/%s" % (
    runfiles_root, workspace_name, runfile_path)

مسیر یک File در پوشه runfiles مربوط به File.short_path است.

باینری که مستقیماً توسط bazel اجرا می شود در مجاورت ریشه دایرکتوری runfiles قرار دارد. با این حال، باینری های فراخوانی شده از فایل های اجرا شده نمی توانند همان فرض را داشته باشند. برای کاهش این موضوع، هر باینری باید راهی برای پذیرش ریشه فایل‌های اجرا شده خود به عنوان پارامتر با استفاده از یک محیط یا آرگومان/پرچم خط فرمان ارائه دهد. این به باینری‌ها اجازه می‌دهد تا ریشه فایل‌های معمولی صحیح را به باینری‌هایی که فراخوانی می‌کند منتقل کنند. اگر تنظیم نشده باشد، یک باینری می تواند حدس بزند که اولین باینری فراخوانی شده است و به دنبال یک فهرست فایل های اجرا شده مجاور بگردد.

موضوعات پیشرفته

درخواست فایل های خروجی

یک هدف می تواند چندین فایل خروجی داشته باشد. هنگامی که یک دستور bazel build اجرا می شود، برخی از خروجی های اهداف داده شده به دستور درخواستی در نظر گرفته می شود. بازل فقط این فایل های درخواستی و فایل هایی را می سازد که مستقیم یا غیرمستقیم به آنها وابسته هستند. (از نظر نمودار عملکرد، Bazel فقط اقداماتی را اجرا می کند که به عنوان وابستگی های انتقالی فایل های درخواستی قابل دسترسی هستند.)

علاوه بر خروجی های پیش فرض ، هر خروجی از پیش اعلام شده را می توان به صراحت در خط فرمان درخواست کرد. قوانین می توانند خروجی های از پیش اعلام شده را از طریق ویژگی های خروجی مشخص کنند. در آن صورت، کاربر به صراحت برچسب‌هایی را برای خروجی‌ها انتخاب می‌کند که قانون را نمونه‌سازی می‌کنند. برای به دست آوردن اشیاء File برای ویژگی های خروجی، از ویژگی مربوطه ctx.outputs استفاده کنید. قوانین می توانند به طور ضمنی خروجی های از پیش اعلام شده را بر اساس نام هدف نیز تعریف کنند، اما این ویژگی منسوخ شده است.

علاوه بر خروجی های پیش فرض، گروه های خروجی نیز وجود دارد که مجموعه ای از فایل های خروجی هستند که ممکن است با هم درخواست شوند. اینها را می توان با --output_groups درخواست کرد. برای مثال، اگر یک هدف //pkg:mytarget از نوع قانون باشد که دارای گروه خروجی debug_files است، این فایل‌ها را می‌توان با اجرای bazel build //pkg:mytarget --output_groups=debug_files . از آنجایی که خروجی های از پیش اعلام نشده برچسب ندارند، فقط می توان آنها را با ظاهر شدن در خروجی های پیش فرض یا یک گروه خروجی درخواست کرد.

گروه های خروجی را می توان با ارائه دهنده OutputGroupInfo مشخص کرد. توجه داشته باشید که برخلاف بسیاری از ارائه دهندگان داخلی، OutputGroupInfo می تواند پارامترهایی را با نام دلخواه برای تعریف گروه های خروجی با آن نام بگیرد:

def _example_library_impl(ctx):
    ...
    debug_file = ctx.actions.declare_file(name + ".pdb")
    ...
    return [
        DefaultInfo(files = depset([output_file]), ...),
        OutputGroupInfo(
            debug_files = depset([debug_file]),
            all_files = depset([output_file, debug_file]),
        ),
        ...
    ]

همچنین برخلاف اکثر ارائه دهندگان، OutputGroupInfo را می توان هم توسط یک جنبه و هم با هدف قانون که آن جنبه به آن اعمال می شود، بازگرداند، تا زمانی که گروه های خروجی یکسانی را تعریف نکنند. در آن صورت، ارائه دهندگان حاصل با هم ادغام می شوند.

توجه داشته باشید که OutputGroupInfo معمولاً نباید برای انتقال انواع خاصی از فایل ها از یک هدف به اقدامات مصرف کنندگان آن استفاده شود. در عوض ارائه دهندگان قانون خاص را برای آن تعریف کنید.

پیکربندی

تصور کنید که می خواهید یک باینری C++ برای معماری متفاوت بسازید. ساخت می تواند پیچیده و شامل چندین مرحله باشد. برخی از باینری‌های میانی، مانند کامپایلرها و تولیدکنندگان کد، باید بر روی پلتفرم اجرایی (که می‌تواند میزبان شما یا یک مجری راه دور باشد) اجرا شود. برخی از باینری ها مانند خروجی نهایی باید برای معماری هدف ساخته شوند.

به همین دلیل، بازل مفهومی از «پیکربندی» و انتقال دارد. بالاترین اهداف (آنهایی که در خط فرمان درخواست می شوند) در پیکربندی "هدف" ساخته شده اند، در حالی که ابزارهایی که باید بر روی پلت فرم اجرا اجرا شوند در پیکربندی "exec" ساخته شده اند. قوانین ممکن است اقدامات مختلفی را بر اساس پیکربندی ایجاد کنند، به عنوان مثال برای تغییر معماری cpu که به کامپایلر ارسال می شود. در برخی موارد، ممکن است یک کتابخانه برای پیکربندی های مختلف مورد نیاز باشد. اگر این اتفاق بیفتد، چندین بار آنالیز و به طور بالقوه ساخته خواهد شد.

به‌طور پیش‌فرض، Bazel وابستگی‌های یک هدف را با همان پیکربندی خود هدف، به عبارت دیگر بدون انتقال ایجاد می‌کند. هنگامی که یک وابستگی ابزاری است که برای کمک به ساخت هدف مورد نیاز است، ویژگی مربوطه باید یک انتقال به یک پیکربندی exec را مشخص کند. این باعث می شود که ابزار و تمام وابستگی های آن برای پلت فرم اجرا ساخته شوند.

برای هر ویژگی وابستگی، می‌توانید از cfg استفاده کنید تا تصمیم بگیرید که آیا وابستگی‌ها باید در همان پیکربندی ساخته شوند یا به یک پیکربندی exec منتقل شوند. اگر یک ویژگی وابستگی دارای flag executable=True باشد، cfg باید به صراحت تنظیم شود. این برای محافظت در برابر ساخت تصادفی ابزاری برای پیکربندی اشتباه است. نمونه را ببینید

به طور کلی، منابع، کتابخانه‌های وابسته، و فایل‌های اجرایی که در زمان اجرا مورد نیاز خواهند بود، می‌توانند از همان پیکربندی استفاده کنند.

ابزارهایی که به عنوان بخشی از ساخت اجرا می شوند (مانند کامپایلرها یا تولیدکنندگان کد) باید برای پیکربندی exec ساخته شوند. در این مورد، cfg="exec" را در ویژگی مشخص کنید.

در غیر این صورت، فایل های اجرایی که در زمان اجرا استفاده می شوند (مانند بخشی از یک تست) باید برای پیکربندی هدف ساخته شوند. در این حالت cfg="target" را در ویژگی مشخص کنید.

cfg="target" در واقع هیچ کاری انجام نمی دهد: این صرفاً یک ارزش راحت است که به طراحان قوانین کمک می کند تا در مورد اهداف خود صریح باشند. وقتی executable=False ، به این معنی که cfg اختیاری است، فقط زمانی این را تنظیم کنید که واقعاً به خوانایی کمک کند.

همچنین می‌توانید از cfg=my_transition برای استفاده از انتقال‌های تعریف‌شده توسط کاربر استفاده کنید، که به نویسندگان قوانین انعطاف‌پذیری زیادی در تغییر پیکربندی‌ها می‌دهد، با این اشکال که نمودار ساخت را بزرگ‌تر و کمتر قابل درک می‌کند.

نکته : از لحاظ تاریخی، Bazel مفهوم پلتفرم‌های اجرا را نداشت و در عوض تمام اقدامات ساخت در ماشین میزبان اجرا می‌شد. به همین دلیل، یک پیکربندی "میزبان" منفرد و یک انتقال "میزبان" وجود دارد که می تواند برای ایجاد یک وابستگی در پیکربندی میزبان استفاده شود. بسیاری از قوانین هنوز از انتقال "میزبان" برای ابزارهای خود استفاده می کنند، اما در حال حاضر منسوخ شده است و در صورت امکان برای استفاده از انتقال "exec" منتقل می شود.

تفاوت های زیادی بین پیکربندی "host" و "exec" وجود دارد:

  • "host" ترمینال است، "exec" نیست: هنگامی که یک وابستگی در پیکربندی "host" قرار گرفت، هیچ انتقال دیگری مجاز نیست. هنگامی که در یک پیکربندی "exec" قرار گرفتید، می توانید به انجام انتقال های پیکربندی بیشتر ادامه دهید.
  • "host" یکپارچه است، "exec" نیست: فقط یک پیکربندی "host" وجود دارد، اما می‌تواند یک پیکربندی "exec" متفاوت برای هر پلتفرم اجرایی وجود داشته باشد.
  • "میزبان" فرض می کند که شما ابزارها را بر روی همان ماشینی با Bazel یا روی یک ماشین بسیار مشابه اجرا می کنید. این دیگر درست نیست: می‌توانید اکشن‌های ساخت را بر روی ماشین محلی خود یا روی یک مجری راه دور اجرا کنید، و هیچ تضمینی وجود ندارد که مجری راه دور همان CPU و OS دستگاه محلی شما باشد.

هر دو پیکربندی "exec" و "host" تغییرات گزینه مشابهی را اعمال می کنند، (به عنوان مثال، --compilation_mode را از --host_compilation_mode تنظیم کنید، --cpu را از --host_cpu و غیره را تنظیم کنید). تفاوت این است که پیکربندی «میزبان» با مقادیر پیش‌فرض همه پرچم‌های دیگر شروع می‌شود، در حالی که پیکربندی «exec» با مقادیر فعلی پرچم‌ها بر اساس پیکربندی هدف شروع می‌شود.

قطعات پیکربندی

قوانین ممکن است به قطعات پیکربندی مانند cpp ، java و jvm دسترسی داشته باشند. با این حال، تمام قطعات مورد نیاز باید برای جلوگیری از خطاهای دسترسی اعلان شوند:

def _impl(ctx):
    # Using ctx.fragments.cpp leads to an error since it was not declared.
    x = ctx.fragments.java
    ...

my_rule = rule(
    implementation = _impl,
    fragments = ["java"],      # Required fragments of the target configuration
    host_fragments = ["java"], # Required fragments of the host configuration
    ...
)

ctx.fragments فقط قطعات پیکربندی را برای پیکربندی هدف فراهم می کند. اگر می خواهید به قطعات برای پیکربندی میزبان دسترسی داشته باشید، به جای آن از ctx.host_fragments استفاده کنید.

به طور معمول، مسیر نسبی یک فایل در درخت runfiles با مسیر نسبی آن فایل در درخت منبع یا درخت خروجی تولید شده یکسان است. اگر بنا به دلایلی لازم است اینها متفاوت باشند، می توانید آرگومان های root_symlinks یا symlinks را مشخص کنید. root_symlinks یک فرهنگ لغت است که مسیرهای فایل‌ها را نگاشت می‌کند، جایی که مسیرها نسبت به ریشه دایرکتوری runfiles هستند. فرهنگ لغت symlinks یکسان است، اما مسیرها به طور ضمنی با نام فضای کاری پیشوند هستند.

    ...
    runfiles = ctx.runfiles(
        root_symlinks = {"some/path/here.foo": ctx.file.some_data_file2}
        symlinks = {"some/path/here.bar": ctx.file.some_data_file3}
    )
    # Creates something like:
    # sometarget.runfiles/
    #     some/
    #         path/
    #             here.foo -> some_data_file2
    #     <workspace_name>/
    #         some/
    #             path/
    #                 here.bar -> some_data_file3

اگر از symlinks یا root_symlinks استفاده می‌شود، مراقب باشید که دو فایل مختلف را به یک مسیر در درخت runfiles نگاشت نکنید. این باعث می شود که ساخت با خطا در توصیف تضاد شکست بخورد. برای رفع مشکل، باید آرگومان های ctx.runfiles خود را تغییر دهید تا برخورد حذف شود. این بررسی برای هر هدفی که از قانون شما استفاده می‌کند، و همچنین اهدافی از هر نوعی که به آن اهداف بستگی دارد، انجام می‌شود. این امر به ویژه در صورتی خطرناک است که ابزار شما احتمالاً توسط ابزار دیگری به صورت گذرا مورد استفاده قرار گیرد. نام‌های پیوند نمادین باید در میان فایل‌های اجرایی یک ابزار و همه وابستگی‌های آن منحصربه‌فرد باشند.

پوشش کد

هنگامی که فرمان coverage اجرا می شود، ساخت ممکن است نیاز به افزودن ابزار دقیق پوشش برای اهداف خاصی داشته باشد. این بیلد همچنین فهرستی از فایل‌های منبع را که ابزارسازی شده‌اند جمع‌آوری می‌کند. زیرمجموعه اهدافی که در نظر گرفته می شوند توسط flag --instrumentation_filter کنترل می شوند. اهداف آزمایشی مستثنی هستند، مگر اینکه --instrument_test_targets مشخص شده باشد.

اگر اجرای یک قانون ابزار دقیق پوشش را در زمان ساخت اضافه کند، باید آن را در عملکرد پیاده‌سازی خود در نظر بگیرد. ctx.coverage_instrumented در حالت پوشش مقدار true را برمی‌گرداند اگر منابع هدف باید ابزاری باشند:

# Are this rule's sources instrumented?
if ctx.coverage_instrumented():
  # Do something to turn on coverage for this compile action

منطقی که همیشه باید در حالت پوشش روشن باشد (خواه منابع یک هدف به طور خاص دارای ابزار باشند یا نه) می‌تواند مشروط به ctx.configuration.coverage_enabled باشد.

اگر قانون مستقیماً شامل منابعی از وابستگی‌های خود قبل از کامپایل باشد (مانند فایل‌های سرصفحه)، ممکن است نیاز باشد ابزار دقیق زمان کامپایل را نیز روشن کند، اگر منابع وابستگی‌ها باید ابزاری باشند:

# Are this rule's sources or any of the sources for its direct dependencies
# in deps instrumented?
if (ctx.configuration.coverage_enabled and
    (ctx.coverage_instrumented() or
     any([ctx.coverage_instrumented(dep) for dep in ctx.attr.deps]))):
    # Do something to turn on coverage for this compile action

قوانین همچنین باید اطلاعاتی در مورد اینکه کدام ویژگی‌ها برای پوشش با ارائه‌دهنده InstrumentedFilesInfo مرتبط هستند، که با استفاده از coverage_common.instrumented_files_info ساخته شده‌اند، ارائه دهند. پارامتر dependency_attributes instrumented_files_info باید همه ویژگی‌های وابستگی زمان اجرا، از جمله وابستگی‌های کد مانند deps و وابستگی‌های داده مانند data فهرست کند. اگر ابزار پوششی اضافه شود، پارامتر source_attributes باید ویژگی‌های فایل منبع قانون را فهرست کند:

def _example_library_impl(ctx):
    ...
    return [
        ...
        coverage_common.instrumented_files_info(
            ctx,
            dependency_attributes = ["deps", "data"],
            # Omitted if coverage is not supported for this rule:
            source_attributes = ["srcs", "hdrs"],
        )
        ...
    ]

اگر InstrumentedFilesInfo برگردانده نشود، یک پیش‌فرض با هر ویژگی وابستگی غیرابزاری ایجاد می‌شود که cfg را روی "host" یا "exec" در طرح ویژگی تنظیم نمی‌کند) در dependency_attributes . (این رفتار ایده آل نیست، زیرا ویژگی هایی مانند srcs در dependency_attributes به جای source_attributes قرار می دهد، اما از نیاز به پیکربندی پوشش صریح برای همه قوانین در زنجیره وابستگی جلوگیری می کند.)

اقدامات اعتبارسنجی

گاهی اوقات لازم است چیزی را در مورد ساخت تایید کنید، و اطلاعات مورد نیاز برای انجام آن اعتبارسنجی فقط در مصنوعات (فایل های منبع یا فایل های تولید شده) موجود است. از آنجا که این اطلاعات در مصنوعات هستند، قوانین نمی توانند این اعتبار را در زمان تجزیه و تحلیل انجام دهند زیرا قوانین نمی توانند فایل ها را بخوانند. در عوض، اقدامات باید این اعتبار سنجی را در زمان اجرا انجام دهند. هنگامی که اعتبار سنجی ناموفق باشد، عمل شکست خواهد خورد، و از این رو ساخت نیز شکست خواهد خورد.

نمونه‌هایی از اعتبارسنجی‌هایی که ممکن است اجرا شوند عبارتند از تجزیه و تحلیل استاتیک، پرده‌بندی، بررسی وابستگی و سازگاری، و بررسی سبک.

اقدامات اعتبارسنجی همچنین می‌تواند با انتقال بخش‌هایی از کنش‌هایی که برای ساخت مصنوع‌ها به کنش‌های جداگانه لازم نیستند، به بهبود عملکرد ساخت کمک کند. به عنوان مثال، اگر یک اکشن منفرد که کامپایل و لینتینگ را انجام می‌دهد را بتوان به یک اکشن کامپایل و یک عمل پردهی تفکیک کرد، آن‌گاه اکشن لینتینگ را می‌توان به عنوان یک اقدام اعتبارسنجی اجرا کرد و به موازات سایر اقدامات اجرا شد.

این «اقدامات اعتبارسنجی» اغلب چیزی را تولید نمی‌کنند که در جای دیگری از ساخت‌وساز استفاده شود، زیرا آنها فقط باید چیزهایی را درباره ورودی‌های خود بیان کنند. با این حال، این یک مشکل را ایجاد می کند: اگر یک عمل اعتبارسنجی چیزی را که در جای دیگری از ساخت استفاده می شود تولید نمی کند، چگونه یک قانون باعث اجرای عمل می شود؟ Historically, the approach was to have the validation action output an empty file, and artificially add that output to the inputs of some other important action in the build:

This works, because Bazel will always run the validation action when the compile action is run, but this has significant drawbacks:

  1. The validation action is in the critical path of the build. Because Bazel thinks the empty output is required to run the compile action, it will run the validation action first, even though the compile action will ignore the input. This reduces parallelism and slows down builds.

  2. If other actions in the build might run instead of the compile action, then the empty outputs of validation actions need to be added to those actions as well ( java_library 's source jar output, for example). This is also a problem if new actions that might run instead of the compile action are added later, and the empty validation output is accidentally left off.

The solution to these problems is to use the Validations Output Group.

Validations Output Group

The Validations Output Group is an output group designed to hold the otherwise unused outputs of validation actions, so that they don't need to be artificially added to the inputs of other actions.

This group is special in that its outputs are always requested, regardless of the value of the --output_groups flag, and regardless of how the target is depended upon (for example, on the command line, as a dependency, or through implicit outputs of the target). Note that normal caching and incrementality still apply: if the inputs to the validation action have not changed and the validation action previously succeeded, then the validation action will not be run.

Using this output group still requires that validation actions output some file, even an empty one. This might require wrapping some tools that normally don't create outputs so that a file is created.

A target's validation actions are not run in three cases:

  • When the target is depended upon as a tool
  • When the target is depended upon as an implicit dependency (for example, an attribute that starts with "_")
  • When the target is built in the host or exec configuration.

It is assumed that these targets have their own separate builds and tests that would uncover any validation failures.

Using the Validations Output Group

The Validations Output Group is named _validation and is used like any other output group:

def _rule_with_validation_impl(ctx):

  ctx.actions.write(ctx.outputs.main, "main output\n")

  ctx.actions.write(ctx.outputs.implicit, "implicit output\n")

  validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation")
  ctx.actions.run(
      outputs = [validation_output],
      executable = ctx.executable._validation_tool,
      arguments = [validation_output.path])

  return [
    DefaultInfo(files = depset([ctx.outputs.main])),
    OutputGroupInfo(_validation = depset([validation_output])),
  ]


rule_with_validation = rule(
  implementation = _rule_with_validation_impl,
  outputs = {
    "main": "%{name}.main",
    "implicit": "%{name}.implicit",
  },
  attrs = {
    "_validation_tool": attr.label(
        default = Label("//validation_actions:validation_tool"),
        executable = True,
        cfg = "exec"),
  }
)

Notice that the validation output file is not added to the DefaultInfo or the inputs to any other action. The validation action for a target of this rule kind will still run if the target is depended upon by label, or any of the target's implicit outputs are directly or indirectly depended upon.

It is usually important that the outputs of validation actions only go into the validation output group, and are not added to the inputs of other actions, as this could defeat parallelism gains. Note however that Bazel does not currently have any special checking to enforce this. Therefore, you should test that validation action outputs are not added to the inputs of any actions in the tests for Starlark rules. For example:

load("@bazel_skylib//lib:unittest.bzl", "analysistest")

def _validation_outputs_test_impl(ctx):
  env = analysistest.begin(ctx)

  actions = analysistest.target_actions(env)
  target = analysistest.target_under_test(env)
  validation_outputs = target.output_groups._validation.to_list()
  for action in actions:
    for validation_output in validation_outputs:
      if validation_output in action.inputs.to_list():
        analysistest.fail(env,
            "%s is a validation action output, but is an input to action %s" % (
                validation_output, action))

  return analysistest.end(env)

validation_outputs_test = analysistest.make(_validation_outputs_test_impl)

Validation Actions Flag

Running validation actions is controlled by the --run_validations command line flag, which defaults to true.

Deprecated features

Deprecated predeclared outputs

There are two deprecated ways of using predeclared outputs:

  • The outputs parameter of rule specifies a mapping between output attribute names and string templates for generating predeclared output labels. Prefer using non-predeclared outputs and explicitly adding outputs to DefaultInfo.files . Use the rule target's label as input for rules which consume the output instead of a predeclared output's label.

  • For executable rules , ctx.outputs.executable refers to a predeclared executable output with the same name as the rule target. Prefer declaring the output explicitly, for example with ctx.actions.declare_file(ctx.label.name) , and ensure that the command that generates the executable sets its permissions to allow execution. Explicitly pass the executable output to the executable parameter of DefaultInfo .

Runfiles features to avoid

ctx.runfiles and the runfiles type have a complex set of features, many of which are kept for legacy reasons. The following recommendations help reduce complexity:

  • Avoid use of the collect_data and collect_default modes of ctx.runfiles . These modes implicitly collect runfiles across certain hardcoded dependency edges in confusing ways. Instead, add files using the files or transitive_files parameters of ctx.runfiles , or by merging in runfiles from dependencies with runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles) .

  • Avoid use of the data_runfiles and default_runfiles of the DefaultInfo constructor. Specify DefaultInfo(runfiles = ...) instead. The distinction between "default" and "data" runfiles is maintained for legacy reasons. For example, some rules put their default outputs in data_runfiles , but not default_runfiles . Instead of using data_runfiles , rules should both include default outputs and merge in default_runfiles from attributes which provide runfiles (often data ).

  • When retrieving runfiles from DefaultInfo (generally only for merging runfiles between the current rule and its dependencies), use DefaultInfo.default_runfiles , not DefaultInfo.data_runfiles .

Migrating from legacy providers

Historically, Bazel providers were simple fields on the Target object. They were accessed using the dot operator, and they were created by putting the field in a struct returned by the rule's implementation function.

This style is deprecated and should not be used in new code; see below for information that may help you migrate. The new provider mechanism avoids name clashes. It also supports data hiding, by requiring any code accessing a provider instance to retrieve it using the provider symbol.

For the moment, legacy providers are still supported. A rule can return both legacy and modern providers as follows:

def _old_rule_impl(ctx):
  ...
  legacy_data = struct(x="foo", ...)
  modern_data = MyInfo(y="bar", ...)
  # When any legacy providers are returned, the top-level returned value is a
  # struct.
  return struct(
      # One key = value entry for each legacy provider.
      legacy_info = legacy_data,
      ...
      # Additional modern providers:
      providers = [modern_data, ...])

If dep is the resulting Target object for an instance of this rule, the providers and their contents can be retrieved as dep.legacy_info.x and dep[MyInfo].y .

In addition to providers , the returned struct can also take several other fields that have special meaning (and thus do not create a corresponding legacy provider):

  • The fields files , runfiles , data_runfiles , default_runfiles , and executable correspond to the same-named fields of DefaultInfo . It is not allowed to specify any of these fields while also returning a DefaultInfo provider.

  • The field output_groups takes a struct value and corresponds to an OutputGroupInfo .

In provides declarations of rules, and in providers declarations of dependency attributes, legacy providers are passed in as strings and modern providers are passed in by their *Info symbol. Be sure to change from strings to symbols when migrating. For complex or large rule sets where it is difficult to update all rules atomically, you may have an easier time if you follow this sequence of steps:

  1. Modify the rules that produce the legacy provider to produce both the legacy and modern providers, using the above syntax. For rules that declare they return the legacy provider, update that declaration to include both the legacy and modern providers.

  2. Modify the rules that consume the legacy provider to instead consume the modern provider. If any attribute declarations require the legacy provider, also update them to instead require the modern provider. Optionally, you can interleave this work with step 1 by having consumers accept/require either provider: Test for the presence of the legacy provider using hasattr(target, 'foo') , or the new provider using FooInfo in target .

  3. Fully remove the legacy provider from all rules.