این صفحه چارچوب زنجیره ابزار را توصیف می کند، که راهی برای نویسندگان قوانین است تا منطق قوانین خود را از انتخاب ابزارهای مبتنی بر پلت فرم جدا کنند. توصیه می شود قبل از ادامه، صفحات قوانین و پلتفرم ها را مطالعه کنید. این صفحه به این موضوع میپردازد که چرا به زنجیرههای ابزار نیاز است، نحوه تعریف و استفاده از آنها، و نحوه انتخاب زنجیره ابزار مناسب بر اساس محدودیتهای پلتفرم توسط Bazel.
انگیزه
بیایید ابتدا به مشکلاتی که زنجیره ابزار برای حل آنها طراحی شده اند نگاه کنیم. فرض کنید در حال نوشتن قوانینی برای پشتیبانی از زبان برنامه نویسی "bar" هستید. قانون bar_binary شما فایل های *.bar را با استفاده از کامپایلر barc کامپایل می کند، ابزاری که خود به عنوان هدف دیگری در فضای کاری شما ساخته شده است. از آنجایی که کاربرانی که اهداف bar_binary را می نویسند نباید وابستگی به کامپایلر تعیین کنند، شما با اضافه کردن آن به تعریف قانون به عنوان یک ویژگی خصوصی، آن را به یک وابستگی ضمنی تبدیل می کنید.
bar_binary = rule(
implementation = _bar_binary_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
...
"_compiler": attr.label(
default = "//bar_tools:barc_linux", # the compiler running on linux
providers = [BarcInfo],
),
},
)
//bar_tools:barc_linux اکنون وابستگی هر هدف bar_binary است، بنابراین قبل از هر هدف bar_binary ساخته میشود. با تابع اجرای قانون می توان به آن مانند هر ویژگی دیگر دسترسی داشت:
BarcInfo = provider(
doc = "Information about how to invoke the barc compiler.",
# In the real world, compiler_path and system_lib might hold File objects,
# but for simplicity they are strings for this example. arch_flags is a list
# of strings.
fields = ["compiler_path", "system_lib", "arch_flags"],
)
def _bar_binary_impl(ctx):
...
info = ctx.attr._compiler[BarcInfo]
command = "%s -l %s %s" % (
info.compiler_path,
info.system_lib,
" ".join(info.arch_flags),
)
...
مسئله اینجاست که برچسب کامپایلر به صورت bar_binary شده است، اما اهداف مختلف بسته به اینکه برای چه پلتفرمی ساخته میشوند و بر روی چه پلتفرمی ساخته میشوند، ممکن است به کامپایلرهای متفاوتی نیاز داشته باشند که به ترتیب پلتفرم هدف و پلت فرم اجرا نامیده میشوند. علاوه بر این، نویسنده قانون لزوماً حتی همه ابزارها و پلتفرمهای موجود را نمیداند، بنابراین نمیتوان آنها را در تعریف قانون کدگذاری کرد.
یک راه حل کمتر از ایده آل این است که با غیر خصوصی کردن ویژگی _compiler ، بار را بر دوش کاربران بگذاریم. سپس اهداف منفرد را می توان برای ساختن برای یک پلتفرم یا پلتفرم دیگر کدگذاری کرد.
bar_binary(
name = "myprog_on_linux",
srcs = ["mysrc.bar"],
compiler = "//bar_tools:barc_linux",
)
bar_binary(
name = "myprog_on_windows",
srcs = ["mysrc.bar"],
compiler = "//bar_tools:barc_windows",
)
می توانید با استفاده از select برای انتخاب compiler بر اساس پلتفرم، این راه حل را بهبود ببخشید:
config_setting(
name = "on_linux",
constraint_values = [
"@platforms//os:linux",
],
)
config_setting(
name = "on_windows",
constraint_values = [
"@platforms//os:windows",
],
)
bar_binary(
name = "myprog",
srcs = ["mysrc.bar"],
compiler = select({
":on_linux": "//bar_tools:barc_linux",
":on_windows": "//bar_tools:barc_windows",
}),
)
اما این خسته کننده است و از تک تک کاربران bar_binary . اگر این سبک به طور مداوم در سرتاسر فضای کاری مورد استفاده قرار نگیرد، منجر به ساختهایی میشود که روی یک پلتفرم خوب کار میکنند، اما وقتی به سناریوهای چند پلتفرمی گسترش مییابند، شکست میخورند. همچنین مشکل اضافه کردن پشتیبانی برای پلتفرمها و کامپایلرهای جدید بدون تغییر قوانین یا اهداف موجود را برطرف نمیکند.
چارچوب زنجیره ابزار این مشکل را با اضافه کردن یک سطح اضافی از غیر جهت حل می کند. اساساً، شما اعلام میکنید که قانون شما به برخی از اعضای یک خانواده از اهداف (نوع زنجیره ابزار) وابستگی انتزاعی دارد و Bazel به طور خودکار این موضوع را به یک هدف خاص (یک زنجیره ابزار) بر اساس محدودیتهای پلتفرم قابل اجرا حل میکند. نه نویسنده قانون و نه نویسنده هدف نیازی به دانستن مجموعه کامل پلتفرمها و زنجیرههای ابزار موجود ندارند.
نوشتن قوانینی که از زنجیره ابزار استفاده می کنند
در چارچوب زنجیره ابزار، به جای اینکه قوانینی که مستقیماً به ابزارها وابسته باشند، در عوض به انواع زنجیره ابزار وابسته هستند. نوع زنجیره ابزار یک هدف ساده است که نشان دهنده دسته ای از ابزارها است که نقش یکسانی را برای پلتفرم های مختلف ایفا می کنند. به عنوان مثال، می توانید نوعی را که نشان دهنده کامپایلر نوار است، اعلام کنید:
# By convention, toolchain_type targets are named "toolchain_type" and
# distinguished by their package path. So the full path for this would be
# //bar_tools:toolchain_type.
toolchain_type(name = "toolchain_type")
تعریف قانون در بخش قبل به گونهای اصلاح شده است که به جای اینکه کامپایلر را به عنوان یک ویژگی در نظر بگیرد، اعلام میکند که یک زنجیره ابزار //bar_tools:toolchain_type را مصرف میکند.
bar_binary = rule(
implementation = _bar_binary_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
...
# No `_compiler` attribute anymore.
},
toolchains = ["//bar_tools:toolchain_type"],
)
تابع پیاده سازی اکنون به این وابستگی در زیر ctx.toolchains به جای ctx.attr دسترسی پیدا می کند و از نوع toolchain به عنوان کلید استفاده می کند.
def _bar_binary_impl(ctx):
...
info = ctx.toolchains["//bar_tools:toolchain_type"].barcinfo
# The rest is unchanged.
command = "%s -l %s %s" % (
info.compiler_path,
info.system_lib,
" ".join(info.arch_flags),
)
...
ctx.toolchains["//bar_tools:toolchain_type"] ارائهدهنده ToolchainInfo را از هر هدفی که Bazel وابستگی زنجیره ابزار را حل کرده است، برمیگرداند. فیلدهای شی ToolchainInfo توسط قانون ابزار اصلی تنظیم می شوند. در بخش بعدی، این قانون به گونه ای تعریف می شود که یک فیلد barcinfo وجود دارد که یک شی BarcInfo را می پیچد.
روش Bazel برای حل زنجیره ابزار به اهداف در زیر توضیح داده شده است. فقط هدف زنجیره ابزار حل شده در واقع به هدف bar_binary وابسته است، نه کل فضای زنجیره ابزار کاندید.
زنجیره ابزار اجباری و اختیاری
بهطور پیشفرض، وقتی یک قانون وابستگی نوع زنجیره ابزار را با استفاده از یک برچسب خالی بیان میکند (همانطور که در بالا نشان داده شده است)، نوع زنجیره ابزار اجباری در نظر گرفته میشود. اگر Bazel نتواند یک زنجیره ابزار منطبق را برای یک نوع زنجیره ابزار اجباری پیدا کند (به وضوح Toolchain زیر مراجعه کنید)، این یک خطا است و تجزیه و تحلیل متوقف می شود.
به جای آن میتوان یک نوع وابستگی اختیاری زنجیره ابزار را به شرح زیر اعلام کرد:
bar_binary = rule(
...
toolchains = [
config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
],
)
وقتی یک نوع زنجیره ابزار اختیاری قابل حل نباشد، تجزیه و تحلیل ادامه می یابد و نتیجه ctx.toolchains[""//bar_tools:toolchain_type"] None است.
تابع config_common.toolchain_type به طور پیش فرض اجباری است.
از فرم های زیر می توان استفاده کرد:
- انواع زنجیره ابزار اجباری:
-
toolchains = ["//bar_tools:toolchain_type"] -
toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type")] -
toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = True)]
-
- انواع زنجیره ابزار اختیاری:
-
toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False)]
-
bar_binary = rule(
...
toolchains = [
"//foo_tools:toolchain_type",
config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
],
)
همچنین میتوانید فرمها را در همان قانون ترکیب و مطابقت دهید. با این حال، اگر یک نوع زنجیره ابزار چندین بار فهرست شود، سختترین نسخه را میگیرد، که در آن اجباری سختتر از اختیاری است.
جنبه های نوشتاری که از زنجیره ابزار استفاده می کنند
جنبهها به همان API زنجیره ابزار مانند قوانین دسترسی دارند: میتوانید انواع زنجیره ابزار مورد نیاز را تعریف کنید، به زنجیرههای ابزار از طریق زمینه دسترسی داشته باشید و از آنها برای ایجاد اقدامات جدید با استفاده از زنجیره ابزار استفاده کنید.
bar_aspect = aspect(
implementation = _bar_aspect_impl,
attrs = {},
toolchains = ['//bar_tools:toolchain_type'],
)
def _bar_aspect_impl(target, ctx):
toolchain = ctx.toolchains['//bar_tools:toolchain_type']
# Use the toolchain provider like in a rule.
return []
تعریف زنجیره ابزار
برای تعریف چند زنجیره ابزار برای یک نوع زنجیره ابزار معین، به سه چیز نیاز دارید:
یک قانون خاص زبان که نشان دهنده نوع ابزار یا مجموعه ابزار است. طبق قرارداد نام این قانون با "_toolchain" پسوند است.
- توجه: قانون
\_toolchainنمی تواند هیچ اقدام ساختی ایجاد کند. بلکه مصنوعات را از قوانین دیگر جمع آوری می کند و آنها را به قاعده ای که از زنجیره ابزار استفاده می کند، ارسال می کند. این قانون مسئول ایجاد تمام اقدامات ساخت است.
- توجه: قانون
چندین هدف از این نوع قانون، که نسخههایی از ابزار یا مجموعه ابزار را برای پلتفرمهای مختلف نشان میدهند.
برای هر یک از این اهداف، یک هدف مرتبط از قانون
toolchainعمومی، برای ارائه ابرداده مورد استفاده توسط چارچوب زنجیره ابزار. این هدفtoolchainهمچنین بهtoolchain_typeمرتبط با این زنجیره ابزار اشاره دارد. این بدان معناست که یک قانون_toolchainداده شده را می توان با هرtoolchain_typeمرتبط کرد، و تنها در یک نمونهtoolchainکه از این قانون_toolchainاستفاده می کند، این قانون با یکtoolchain_typeمرتبط است.
برای مثال در حال اجرا، در اینجا تعریفی برای قانون bar_toolchain شده است. مثال ما فقط یک کامپایلر دارد، اما ابزارهای دیگری مانند پیوند دهنده نیز می توانند در زیر آن گروه بندی شوند.
def _bar_toolchain_impl(ctx):
toolchain_info = platform_common.ToolchainInfo(
barcinfo = BarcInfo(
compiler_path = ctx.attr.compiler_path,
system_lib = ctx.attr.system_lib,
arch_flags = ctx.attr.arch_flags,
),
)
return [toolchain_info]
bar_toolchain = rule(
implementation = _bar_toolchain_impl,
attrs = {
"compiler_path": attr.string(),
"system_lib": attr.string(),
"arch_flags": attr.string_list(),
},
)
قانون باید یک ارائهدهنده ToolchainInfo را برگرداند، که به شیئی تبدیل میشود که قانون مصرفکننده با استفاده از ctx.toolchains و برچسب نوع toolchain بازیابی میکند. ToolchainInfo ، مانند struct ، می تواند جفت های فیلد-مقدار دلخواه را در خود جای دهد. مشخصات دقیقاً چه فیلدهایی به ToolchainInfo اضافه می شوند باید به وضوح در نوع Toolchain مستند شوند. در این مثال، مقادیر پیچیده شده در یک شی BarcInfo برای استفاده مجدد از طرح تعریف شده در بالا باز می گردند. این سبک ممکن است برای اعتبارسنجی و استفاده مجدد از کد مفید باشد.
اکنون می توانید اهدافی را برای کامپایلرهای خاص barc تعریف کنید.
bar_toolchain(
name = "barc_linux",
arch_flags = [
"--arch=Linux",
"--debug_everything",
],
compiler_path = "/path/to/barc/on/linux",
system_lib = "/usr/lib/libbarc.so",
)
bar_toolchain(
name = "barc_windows",
arch_flags = [
"--arch=Windows",
# Different flags, no debug support on windows.
],
compiler_path = "C:\\path\\on\\windows\\barc.exe",
system_lib = "C:\\path\\on\\windows\\barclib.dll",
)
در نهایت، برای دو هدف bar_toolchain تعاریف toolchain ایجاد می کنید. این تعاریف اهداف خاص زبان را به نوع زنجیره ابزار مرتبط میکند و اطلاعات محدودیتی را ارائه میکند که به Bazel میگوید چه زمانی زنجیره ابزار برای یک پلتفرم معین مناسب است.
toolchain(
name = "barc_linux_toolchain",
exec_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
target_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
toolchain = ":barc_linux",
toolchain_type = ":toolchain_type",
)
toolchain(
name = "barc_windows_toolchain",
exec_compatible_with = [
"@platforms//os:windows",
"@platforms//cpu:x86_64",
],
target_compatible_with = [
"@platforms//os:windows",
"@platforms//cpu:x86_64",
],
toolchain = ":barc_windows",
toolchain_type = ":toolchain_type",
)
استفاده از نحو مسیر نسبی در بالا نشان میدهد که این تعاریف همه در یک بسته هستند، اما دلیلی وجود ندارد که نوع زنجیره ابزار، اهداف زنجیره ابزار خاص زبان و اهداف تعریف toolchain ، همه در بستههای جداگانه قرار نگیرند.
برای مثال در دنیای واقعی به go_toolchain مراجعه کنید.
زنجیره های ابزار و تنظیمات
یک سوال مهم برای نویسندگان قوانین این است که وقتی یک هدف bar_toolchain تجزیه و تحلیل می شود، چه پیکربندی را می بیند، و چه انتقال هایی باید برای وابستگی ها استفاده شود؟ مثال بالا از ویژگیهای رشتهای استفاده میکند، اما برای یک زنجیره ابزار پیچیدهتر که به اهداف دیگر در مخزن Bazel بستگی دارد، چه اتفاقی میافتد؟
بیایید یک نسخه پیچیده تر از bar_toolchain را ببینیم:
def _bar_toolchain_impl(ctx):
# The implementation is mostly the same as above, so skipping.
pass
bar_toolchain = rule(
implementation = _bar_toolchain_impl,
attrs = {
"compiler": attr.label(
executable = True,
mandatory = True,
cfg = "exec",
),
"system_lib": attr.label(
mandatory = True,
cfg = "target",
),
"arch_flags": attr.string_list(),
},
)
استفاده از attr.label مانند یک قانون استاندارد است، اما معنای پارامتر cfg کمی متفاوت است.
وابستگی از یک هدف (به نام "والد") به یک زنجیره ابزار از طریق رزولوشن زنجیره ابزار از یک انتقال پیکربندی خاص به نام "گذر زنجیره ابزار" استفاده می کند. انتقال زنجیره ابزار پیکربندی را یکسان نگه میدارد، با این تفاوت که پلتفرم اجرایی را مجبور میکند تا برای زنجیره ابزار مانند والد باشد (در غیر این صورت، وضوح زنجیره ابزار برای زنجیره ابزار میتواند هر پلتفرم اجرایی را انتخاب کند، و لزوماً مانند آن نخواهد بود. برای والدین). این اجازه می دهد تا هر وابستگی exec زنجیره ابزار نیز برای اقدامات ساخت والدین قابل اجرا باشد. هر یک از وابستگی های زنجیره ابزار که از cfg = "target" استفاده می کنند (یا cfg را مشخص نمی کنند، زیرا "target" پیش فرض است) برای همان پلت فرم هدف والد ساخته شده اند. این امر به قوانین زنجیره ابزار اجازه می دهد تا کتابخانه ها (ویژگی system_lib در بالا) و ابزارها (ویژگی compiler ) را به قوانین ساختنی که به آنها نیاز دارند کمک کنند. کتابخانههای سیستم به آرتیفکت نهایی متصل میشوند، و بنابراین باید برای همان پلتفرم ساخته شوند، در حالی که کامپایلر ابزاری است که در طول ساخت فراخوانی میشود و باید بتواند روی پلتفرم اجرا اجرا شود.
ثبت و ساخت با زنجیره ابزار
در این مرحله همه بلوکهای سازنده مونتاژ میشوند، و شما فقط باید زنجیرههای ابزار را برای رویه تفکیک Bazel در دسترس قرار دهید. این کار با ثبت کردن زنجیره ابزار، یا در یک فایل WORKSPACE با استفاده از register_toolchains() و یا با ارسال برچسب های toolchains در خط فرمان با استفاده از پرچم --extra_toolchains انجام می شود.
register_toolchains(
"//bar_tools:barc_linux_toolchain",
"//bar_tools:barc_windows_toolchain",
# Target patterns are also permitted, so you could have also written:
# "//bar_tools:all",
)
اکنون وقتی هدفی را می سازید که به نوع زنجیره ابزار بستگی دارد، زنجیره ابزار مناسب بر اساس پلتفرم هدف و اجرا انتخاب می شود.
# my_pkg/BUILD
platform(
name = "my_target_platform",
constraint_values = [
"@platforms//os:linux",
],
)
bar_binary(
name = "my_bar_binary",
...
)
bazel build //my_pkg:my_bar_binary --platforms=//my_pkg:my_target_platform
Bazel می بیند که //my_pkg:my_bar_binary با پلتفرمی ساخته می شود که دارای @platforms//os:linux است و بنابراین مرجع //bar_tools:toolchain_type را به //bar_tools:barc_linux_toolchain می کند. این کار به ساخت //bar_tools:barc_linux اما نه //bar_tools:barc_windows .
وضوح زنجیره ابزار
برای هر هدفی که از زنجیرههای ابزار استفاده میکند، رویه تفکیک زنجیره ابزار Bazel وابستگیهای زنجیره ابزار بتن هدف را تعیین میکند. این رویه مجموعهای از انواع زنجیره ابزار مورد نیاز، پلتفرم هدف، فهرست پلتفرمهای اجرایی موجود و فهرست زنجیرههای ابزار موجود را به عنوان ورودی میگیرد. خروجی های آن یک زنجیره ابزار انتخاب شده برای هر نوع زنجیره ابزار و همچنین یک پلت فرم اجرایی انتخاب شده برای هدف فعلی است.
پلتفرمهای اجرایی موجود و زنجیرههای ابزار از فایل WORKSPACE از طریق register_execution_platforms و register_toolchains جمعآوری میشوند. پلتفرمهای اجرایی اضافی و زنجیرههای ابزار نیز ممکن است در خط فرمان از طریق --extra_execution_platforms و --extra_toolchains . پلت فرم میزبان به طور خودکار به عنوان یک پلت فرم اجرایی موجود گنجانده می شود. پلتفرمها و زنجیرههای ابزار موجود بهعنوان فهرستهای مرتب شده برای جبر ردیابی میشوند و اولویت به موارد قبلی در فهرست داده میشود.
مراحل تفکیک به شرح زیر است.
یک
target_compatible_withیاexec_compatible_withبا یک پلتفرم منطبق است اگر برای هرconstraint_valueدر لیست خود، پلتفرم آنconstraint_valueرا نیز داشته باشد (به طور صریح یا به صورت پیش فرض).اگر پلتفرم دارای
constraint_values ازconstraint_settings باشد که توسط بند ارجاع نشده است، این موارد بر تطابق تأثیری ندارند.اگر هدف در حال ساخت ویژگی
exec_compatible_withرا مشخص کند (یا تعریف قانون آن آرگومانexec_compatible_withرا مشخص کند)، لیست پلتفرمهای اجرایی موجود برای حذف هر کدام که با محدودیتهای اجرا مطابقت ندارد، فیلتر میشود.برای هر پلتفرم اجرایی موجود، هر نوع زنجیره ابزار را با اولین زنجیره ابزار موجود، در صورت وجود، که با این پلتفرم اجرایی و پلتفرم هدف سازگار است، مرتبط میسازید.
هر پلتفرم اجرایی که نتواند یک زنجیره ابزار اجباری سازگار برای یکی از انواع زنجیره ابزار خود پیدا کند، منتفی است. از میان پلتفرمهای باقیمانده، اولین پلتفرم به پلتفرم اجرای هدف فعلی تبدیل میشود و زنجیرههای ابزار مرتبط با آن (در صورت وجود) به وابستگی هدف تبدیل میشوند.
پلت فرم اجرای انتخاب شده برای اجرای تمام اقداماتی که هدف تولید می کند استفاده می شود.
در مواردی که میتوان یک هدف را در پیکربندیهای چندگانه (مانند CPUهای مختلف) در یک بیلد ایجاد کرد، رویه وضوح به طور مستقل برای هر نسخه از هدف اعمال میشود.
اگر قانون از گروههای اجرایی استفاده میکند، هر گروه اجرایی به طور جداگانه وضوح زنجیره ابزار را انجام میدهد و هر کدام پلتفرم اجرایی و زنجیرههای ابزار خاص خود را دارند.
اشکال زدایی زنجیره های ابزار
اگر پشتیبانی زنجیره ابزار را به یک قانون موجود اضافه می کنید، از --toolchain_resolution_debug=regex flag استفاده کنید. در طول تفکیک زنجیره ابزار، پرچم خروجی مفصلی را برای انواع زنجیره ابزار یا نامهای هدفی که با متغیر regex مطابقت دارند ارائه میکند. برای خروجی تمام اطلاعات می توانید از .* استفاده کنید. Bazel نام زنجیرههای ابزاری را که در طول فرآیند حل و فصل بررسی میکند و رد میکند، خروجی میدهد.
اگر میخواهید ببینید کدام وابستگیهای cquery از وضوح ابزار هستند، از پرچم cquery --transitions استفاده کنید:
# Find all direct dependencies of //cc:my_cc_lib. This includes explicitly
# declared dependencies, implicit dependencies, and toolchain dependencies.
$ bazel cquery 'deps(//cc:my_cc_lib, 1)'
//cc:my_cc_lib (96d6638)
@bazel_tools//tools/cpp:toolchain (96d6638)
@bazel_tools//tools/def_parser:def_parser (HOST)
//cc:my_cc_dep (96d6638)
@local_config_platform//:host (96d6638)
@bazel_tools//tools/cpp:toolchain_type (96d6638)
//:default_host_platform (96d6638)
@local_config_cc//:cc-compiler-k8 (HOST)
//cc:my_cc_lib.cc (null)
@bazel_tools//tools/cpp:grep-includes (HOST)
# Which of these are from toolchain resolution?
$ bazel cquery 'deps(//cc:my_cc_lib, 1)' --transitions=lite | grep "toolchain dependency"
[toolchain dependency]#@local_config_cc//:cc-compiler-k8#HostTransition -> b6df211