این صفحه چارچوب زنجیره ابزار را توصیف می کند، که راهی برای نویسندگان قوانین است تا منطق قوانین خود را از انتخاب ابزارهای مبتنی بر پلت فرم جدا کنند. توصیه می شود قبل از ادامه، صفحات قوانین و پلتفرم ها را مطالعه کنید. این صفحه به این موضوع میپردازد که چرا به زنجیرههای ابزار نیاز است، نحوه تعریف و استفاده از آنها، و نحوه انتخاب زنجیره ابزار مناسب بر اساس محدودیتهای پلتفرم توسط 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_value
s ازconstraint_setting
s باشد که توسط بند ارجاع نشده است، این موارد بر تطابق تأثیری ندارند.اگر هدف در حال ساخت ویژگی
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