یک قانون مجموعهای از اقدامات را تعریف میکند که Bazel روی ورودیها برای تولید مجموعهای از خروجیها انجام میدهد، که در ارائهدهندگانی که توسط تابع اجرای قانون برگردانده شدهاند، ارجاع میشوند. به عنوان مثال، یک قانون باینری C++ ممکن است:
- مجموعه ای از فایل های منبع
.cpp
. (ورودی ها) را انتخاب کنید. -
g++
را روی فایل های منبع اجرا کنید (عمل). - ارائهدهنده
DefaultInfo
را با خروجیهای اجرایی و فایلهای دیگر برای در دسترس قرار دادن در زمان اجرا برگردانید. - ارائه دهنده
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.run
، برای اجرای یک فایل اجرایی. -
ctx.actions.run_shell
، برای اجرای یک فرمان پوسته. -
ctx.actions.write
، برای نوشتن یک رشته در یک فایل. -
ctx.actions.expand_template
، برای تولید یک فایل از یک الگو.
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:
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.
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 ofrule
specifies a mapping between output attribute names and string templates for generating predeclared output labels. Prefer using non-predeclared outputs and explicitly adding outputs toDefaultInfo.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 withctx.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 theexecutable
parameter ofDefaultInfo
.
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
andcollect_default
modes ofctx.runfiles
. These modes implicitly collect runfiles across certain hardcoded dependency edges in confusing ways. Instead, add files using thefiles
ortransitive_files
parameters ofctx.runfiles
, or by merging in runfiles from dependencies withrunfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
.Avoid use of the
data_runfiles
anddefault_runfiles
of theDefaultInfo
constructor. SpecifyDefaultInfo(runfiles = ...)
instead. The distinction between "default" and "data" runfiles is maintained for legacy reasons. For example, some rules put their default outputs indata_runfiles
, but notdefault_runfiles
. Instead of usingdata_runfiles
, rules should both include default outputs and merge indefault_runfiles
from attributes which provide runfiles (oftendata
).When retrieving
runfiles
fromDefaultInfo
(generally only for merging runfiles between the current rule and its dependencies), useDefaultInfo.default_runfiles
, notDefaultInfo.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
, andexecutable
correspond to the same-named fields ofDefaultInfo
. It is not allowed to specify any of these fields while also returning aDefaultInfo
provider.The field
output_groups
takes a struct value and corresponds to anOutputGroupInfo
.
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:
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.
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 usingFooInfo in target
.Fully remove the legacy provider from all rules.