با Bzlmod وابستگی های خارجی را مدیریت کنید

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

در Bazel 5.0، Bzlmod به طور پیش فرض روشن نیست. برای اعمال موارد زیر باید پرچم --experimental_enable_bzlmod مشخص شود. همانطور که از نام پرچم پیداست، این ویژگی در حال حاضر آزمایشی است. APIها و رفتارها ممکن است تا زمانی که این ویژگی به طور رسمی راه اندازی شود تغییر کند.

ماژول های بازل

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

یک ماژول در اصل یک پروژه Bazel است که می تواند چندین نسخه داشته باشد، که هر کدام متادیتا را در مورد ماژول های دیگری که به آن وابسته است منتشر می کند. این شبیه به مفاهیم آشنا در سایر سیستم‌های مدیریت وابستگی است: یک مصنوع Maven، یک بسته npm، یک جعبه Cargo، یک ماژول Go و غیره.

یک ماژول به سادگی وابستگی های خود را با استفاده از جفت name و version ، به جای URL های خاص در WORKSPACE مشخص می کند. سپس وابستگی ها در یک رجیستری Bazel جستجو می شوند. به طور پیش فرض، رجیستری مرکزی Bazel . در فضای کاری شما، هر ماژول سپس به یک مخزن تبدیل می شود.

MODULE.bazel

هر نسخه از هر ماژول دارای یک فایل MODULE.bazel است که وابستگی ها و سایر ابرداده ها را اعلام می کند. در اینجا یک مثال اساسی است:

module(
    name = "my-module",
    version = "1.0",
)

bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")

فایل MODULE.bazel باید در ریشه دایرکتوری فضای کاری (در کنار فایل WORKSPACE ) قرار گیرد. برخلاف فایل WORKSPACE ، نیازی به تعیین وابستگی های متعدی خود ندارید. در عوض، شما فقط باید وابستگی های مستقیم را مشخص کنید، و فایل های MODULE.bazel وابستگی های شما برای کشف وابستگی های گذرا به طور خودکار پردازش می شوند.

فایل MODULE.bazel شبیه فایل های BUILD است زیرا از هیچ شکلی از جریان کنترل پشتیبانی نمی کند. علاوه بر این، بیانیه های load را ممنوع می کند. دستورالعمل هایی که از فایل های MODULE.bazel پشتیبانی می کنند عبارتند از:

فرمت نسخه

Bazel دارای اکوسیستم متنوعی است و پروژه ها از طرح های نسخه سازی مختلفی استفاده می کنند. محبوب‌ترین آنها SemVer است، اما پروژه‌های برجسته‌ای نیز وجود دارند که از طرح‌های مختلفی مانند Abseil استفاده می‌کنند، که نسخه‌های آن مبتنی بر تاریخ هستند، برای مثال 20210324.2 ).

به همین دلیل، Bzlmod یک نسخه آرام‌تر از مشخصات SemVer را اتخاذ می‌کند، به ویژه اجازه می‌دهد هر تعداد دنباله‌ای از ارقام را در بخش "انتشار" نسخه (به جای دقیقاً 3 که SemVer تجویز می‌کند: MAJOR.MINOR.PATCH ). علاوه بر این، معنای افزایش نسخه اصلی، جزئی و وصله اعمال نمی شود. (با این حال، سطح سازگاری را برای جزئیات بیشتر در مورد نحوه نشان دادن سازگاری به عقب رجوع کنید.) سایر بخش‌های مشخصات SemVer، مانند خط فاصله که نشان‌دهنده یک نسخه پیش‌نشر است، اصلاح نشده‌اند.

وضوح نسخه

مشکل وابستگی الماس یک اصل اساسی در فضای مدیریت وابستگی نسخه شده است. فرض کنید نمودار وابستگی زیر را دارید:

       A 1.0
      /     \
   B 1.0    C 1.1
     |        |
   D 1.0    D 1.1

از کدام نسخه D باید استفاده کرد؟ برای حل این سوال، Bzlmod از الگوریتم Minimal Version Selection (MVS) معرفی شده در سیستم ماژول Go استفاده می کند. MVS فرض می‌کند که تمام نسخه‌های جدید یک ماژول با عقب سازگار هستند، و بنابراین به سادگی بالاترین نسخه مشخص‌شده توسط هر وابسته را انتخاب می‌کند (D 1.1 در مثال ما). این "حداقل" نامیده می شود زیرا D 1.1 در اینجا حداقل نسخه ای است که می تواند نیازهای ما را برآورده کند. حتی اگر D 1.2 یا جدیدتر وجود داشته باشد، آنها را انتخاب نمی کنیم. این مزیت افزوده است که انتخاب نسخه با وفاداری بالا و قابل تکرار است.

وضوح نسخه به صورت محلی بر روی دستگاه شما انجام می شود، نه توسط رجیستری.

سطح سازگاری

توجه داشته باشید که فرض MVS در مورد سازگاری با عقب، امکان پذیر است زیرا به سادگی نسخه های ناسازگار یک ماژول را به عنوان یک ماژول جداگانه در نظر می گیرد. از نظر SemVer، این بدان معناست که A 1.x و A 2.x ماژول‌های مجزا در نظر گرفته می‌شوند و می‌توانند در گراف وابستگی حل‌شده همزیستی داشته باشند. این به نوبه خود با این واقعیت امکان پذیر است که نسخه اصلی در مسیر بسته در Go کدگذاری شده است، بنابراین هیچ تضاد زمان کامپایل یا زمان پیوند وجود ندارد.

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

نام مخازن

در بازل، هر وابستگی خارجی یک نام مخزن دارد. گاهی اوقات، ممکن است از یک وابستگی یکسان از طریق نام‌های مخزن مختلف استفاده شود (به عنوان مثال، هر دو @io_bazel_skylib و @bazel_skylib به معنای Bazel skylib هستند )، یا ممکن است از یک نام مخزن برای وابستگی‌های مختلف در پروژه‌های مختلف استفاده شود.

در Bzlmod، مخازن را می توان توسط ماژول های Bazel و افزونه های ماژول تولید کرد. برای حل تداخل نام مخزن، ما در سیستم جدید از مکانیزم نگاشت مخزن استفاده می کنیم. در اینجا دو مفهوم مهم وجود دارد:

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

    • برای مخازن ماژول Bazel: module_name . version
      ( مثال . @bazel_skylib.1.0.3 )
    • برای مخازن پسوند ماژول: module_name . version . extension_name . repo_name
      ( مثال . @rules_cc.0.0.1.cc_configure.local_config_cc )
  • نام مخزن محلی : نام مخزن مورد استفاده در فایل های BUILD و .bzl . در یک مخزن. وابستگی یکسان می‌تواند نام‌های محلی متفاوتی برای مخازن مختلف داشته باشد.
    به شرح زیر تعیین می شود:

    • برای مخازن ماژول Bazel: module_name به طور پیش‌فرض، یا نامی که توسط ویژگی repo_name در bazel_dep مشخص شده است.
    • برای مخازن پسوند ماژول: نام مخزن معرفی شده از طریق use_repo .

هر مخزن دارای یک فرهنگ لغت نگاشت مخزن از وابستگی های مستقیم خود است که نقشه ای از نام مخزن محلی تا نام مخزن متعارف است. ما از نگاشت مخزن برای حل نام مخزن هنگام ساخت یک برچسب استفاده می کنیم. توجه داشته باشید که هیچ تداخلی در نام‌های مخزن متعارف وجود ندارد و استفاده از نام‌های مخزن محلی را می‌توان با تجزیه فایل MODULE.bazel کشف کرد، بنابراین تداخل‌ها را می‌توان به راحتی پیدا کرد و بدون تأثیر بر وابستگی‌های دیگر برطرف کرد.

عمق های سخت

قالب جدید مشخصات وابستگی به ما امکان می دهد بررسی های دقیق تری انجام دهیم. به‌ویژه، اکنون ما اصرار می‌کنیم که یک ماژول فقط می‌تواند از مخازن ایجاد شده از وابستگی‌های مستقیم خود استفاده کند. این به جلوگیری از شکستگی‌های تصادفی و سخت‌زدایی در هنگام تغییر چیزی در نمودار وابستگی گذرا کمک می‌کند.

Strict Deps بر اساس نگاشت مخزن پیاده سازی شده است. اساساً نقشه‌برداری مخزن برای هر مخزن شامل تمام وابستگی‌های مستقیم آن است، هیچ مخزن دیگری قابل مشاهده نیست. وابستگی های قابل مشاهده برای هر مخزن به شرح زیر تعیین می شود:

  • مخزن ماژول Bazel می تواند تمام مخازن معرفی شده در فایل MODULE.bazel را از طریق bazel_dep و use_repo .
  • یک مخزن افزونه ماژول می‌تواند تمام وابستگی‌های قابل مشاهده ماژولی را که افزونه را ارائه می‌کند، به‌علاوه همه مخازن دیگر تولید شده توسط همان افزونه ماژول را ببیند.

دفاتر ثبت

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

ثبت فهرست

رجیستری فهرست یک دایرکتوری محلی یا یک سرور HTTP ثابت است که حاوی اطلاعات فهرستی از ماژول‌ها، از جمله صفحه اصلی، نگهدارنده‌ها، فایل MODULE.bazel هر نسخه و نحوه واکشی منبع هر نسخه است. قابل ذکر است که نیازی به ارائه خود آرشیو منبع ندارد.

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

  • /bazel_registry.json : یک فایل JSON حاوی ابرداده برای رجیستری. در حال حاضر، فقط یک کلید دارد، mirrors ، که فهرستی از آینه‌ها را برای بایگانی منبع مشخص می‌کند.
  • /modules : دایرکتوری حاوی یک زیر شاخه برای هر ماژول در این رجیستری.
  • /modules/$MODULE : دایرکتوری حاوی یک زیر شاخه برای هر نسخه از این ماژول و همچنین فایل زیر:
    • metadata.json : یک فایل JSON حاوی اطلاعات مربوط به ماژول، با فیلدهای زیر:
      • homepage : آدرس صفحه اصلی پروژه.
      • maintainers ها: لیستی از اشیاء JSON که هر کدام مربوط به اطلاعات نگهدارنده ماژول در رجیستری است. توجه داشته باشید که این لزوماً مشابه نویسندگان پروژه نیست.
      • versions ها: فهرستی از تمام نسخه های این ماژول که در این رجیستری یافت می شوند.
      • yanked_versions : لیستی از نسخه های یانک شده این ماژول. این در حال حاضر بدون عملیات است، اما در آینده، نسخه‌های یانک شده نادیده گرفته می‌شوند یا با خطا مواجه می‌شوند.
  • /modules/$MODULE/$VERSION : دایرکتوری حاوی فایل‌های زیر:
    • MODULE.bazel : فایل MODULE.bazel این نسخه ماژول.
    • source.json : یک فایل JSON حاوی اطلاعاتی در مورد نحوه واکشی منبع این نسخه ماژول، با فیلدهای زیر:
      • url : آدرس آرشیو منبع.
      • integrity : جمع کنترلی یکپارچگی زیرمنابع آرشیو.
      • strip_prefix : پیشوند فهرستی که هنگام استخراج بایگانی منبع باید حذف شود.
      • patches : لیستی از رشته ها که هر کدام یک فایل وصله را برای اعمال در بایگانی استخراج شده نام می برند. فایل های وصله در زیر پوشه /modules/$MODULE/$VERSION/patches قرار دارند.
      • patch_strip : همانند آرگومان --strip پچ یونیکس.
    • patches/ : یک دایرکتوری اختیاری حاوی فایل های پچ.

ثبت مرکزی بازل

Bazel Central Registry (BCR) یک فهرست فهرست واقع در registry.bazel.build است. محتویات آن توسط مخزن GitHub bazelbuild/bazel-central-registry شود.

BCR توسط جامعه بازل نگهداری می شود. از مشارکت کنندگان برای ارسال درخواست های کشش استقبال می شود. به سیاست ها و رویه های ثبت مرکزی بازل مراجعه کنید.

علاوه بر پیروی از فرمت یک رجیستری فهرست معمولی، BCR به یک فایل presubmit.yml برای هر نسخه ماژول نیاز دارد ( /modules/$MODULE/$VERSION/presubmit.yml ). این فایل چند هدف اساسی ساخت و آزمایش را مشخص می‌کند که می‌توان از آنها برای بررسی صحت این نسخه ماژول استفاده کرد و توسط خطوط لوله CI BCR برای اطمینان از قابلیت همکاری بین ماژول‌ها در BCR استفاده می‌شود.

انتخاب رجیستری ها

رجیستری با پرچم قابل تکرار --registry را می توان برای تعیین لیست رجیستری ها برای درخواست ماژول ها استفاده کرد، بنابراین می توانید پروژه خود را برای واکشی وابستگی ها از یک رجیستری شخص ثالث یا داخلی تنظیم کنید. ثبت های قبلی اولویت دارند. برای راحتی کار، می توانید لیستی از پرچم های --registry را در فایل .bazelrc . پروژه خود قرار دهید.

برنامه های افزودنی ماژول

افزونه‌های ماژول به شما امکان می‌دهند سیستم ماژول را با خواندن داده‌های ورودی از ماژول‌ها در سراسر نمودار وابستگی، انجام منطق لازم برای حل وابستگی‌ها و در نهایت ایجاد مخازن با فراخوانی قوانین مخزن گسترش دهید. آنها از نظر عملکرد مشابه ماکروهای WORKSPACE امروزی هستند، اما بیشتر در دنیای ماژول ها و وابستگی های انتقالی مناسب هستند.

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

          [ A 1.1                ]
          [   * maven.dep(X 2.1) ]
          [   * maven.pom(...)   ]
              /              \
   bazel_dep /                \ bazel_dep
            /                  \
[ B 1.2                ]     [ C 1.0                ]
[   * maven.dep(X 1.2) ]     [   * maven.dep(X 2.1) ]
[   * maven.dep(Y 1.3) ]     [   * cargo.dep(P 1.1) ]
            \                  /
   bazel_dep \                / bazel_dep
              \              /
          [ D 1.4                ]
          [   * maven.dep(Z 1.4) ]
          [   * cargo.dep(Q 1.1) ]

در نمودار وابستگی مثال بالا، A 1.1 و B 1.2 و غیره ماژول های Bazel هستند. می توانید هر کدام را به عنوان یک فایل MODULE.bazel در نظر بگیرید. هر ماژول می تواند برخی از برچسب ها را برای پسوند ماژول مشخص کند. در اینجا برخی برای پسوند "maven" و برخی برای "محموله" مشخص شده اند. وقتی این نمودار وابستگی نهایی شد (برای مثال، شاید B 1.2 واقعاً یک bazel_dep در D 1.3 داشته باشد اما به دلیل C به D 1.4 ارتقا یافته است)، پسوندهای "maven" اجرا می شوند و می توانند همه برچسب ها را بخوانند maven.* ، از اطلاعات موجود در آن برای تصمیم گیری برای ایجاد مخزن استفاده می کند. به طور مشابه برای پسوند "محموله".

استفاده از پسوند

برنامه‌های افزودنی در خود ماژول‌های Bazel میزبانی می‌شوند، بنابراین برای استفاده از یک افزونه در ماژول خود، ابتدا باید یک bazel_dep را روی آن ماژول اضافه کنید و سپس تابع use_extension را فراخوانی کنید تا آن را در محدوده قرار دهید. مثال زیر را در نظر بگیرید، یک قطعه از یک فایل MODULE.bazel برای استفاده از پسوند فرضی "maven" تعریف شده در ماژول rules_jvm_external :

bazel_dep(name = "rules_jvm_external", version = "1.0")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

پس از وارد کردن پسوند به محدوده، سپس می توانید از دستور نقطه برای تعیین برچسب برای آن استفاده کنید. توجه داشته باشید که برچسب‌ها باید از طرحی که توسط کلاس‌های برچسب مربوطه تعریف شده است پیروی کنند ( تعریف پسوند زیر را ببینید). در اینجا یک مثال برای تعیین برخی از برچسب های maven.dep و maven.pom شده است.

maven.dep(coord="org.junit:junit:3.0")
maven.dep(coord="com.google.guava:guava:1.2")
maven.pom(pom_xml="//:pom.xml")

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

use_repo(
    maven,
    "org_junit_junit",
    guava="com_google_guava_guava",
)

مخازن تولید شده توسط یک افزونه بخشی از API آن هستند، بنابراین از برچسب‌هایی که مشخص کرده‌اید، باید بدانید که افزونه "maven" قرار است مخزنی به نام "org_junit_junit" و یکی به نام "com_google_guava_guava" تولید کند. با use_repo ، می توانید به صورت اختیاری آنها را در محدوده ماژول خود تغییر نام دهید، مانند "گوآوا" در اینجا.

تعریف پسوند

پسوندهای ماژول با استفاده از تابع module_extension مشابه قوانین repo تعریف می شوند. هر دو دارای عملکرد پیاده سازی هستند. اما در حالی که قوانین repo دارای تعدادی ویژگی هستند، پسوندهای ماژول دارای تعدادی tag_class es هستند که هر کدام دارای تعدادی ویژگی است. کلاس‌های برچسب، طرح‌واره‌هایی را برای برچسب‌های استفاده شده توسط این پسوند تعریف می‌کنند. ادامه مثال ما از پسوند فرضی "maven" در بالا:

# @rules_jvm_external//:extensions.bzl
maven_dep = tag_class(attrs = {"coord": attr.string()})
maven_pom = tag_class(attrs = {"pom_xml": attr.label()})
maven = module_extension(
    implementation=_maven_impl,
    tag_classes={"dep": maven_dep, "pom": maven_pom},
)

این اعلان‌ها روشن می‌سازند که maven.dep و maven.pom را می‌توان با استفاده از طرح ویژگی تعریف شده در بالا مشخص کرد.

تابع پیاده‌سازی شبیه یک ماکرو WORKSPACE است، با این تفاوت که یک شی module_ctx دریافت می‌کند که به نمودار وابستگی و همه تگ‌های مربوطه دسترسی می‌دهد. سپس تابع پیاده سازی باید قوانین repo را برای تولید مخازن فراخوانی کند:

# @rules_jvm_external//:extensions.bzl
load("//:repo_rules.bzl", "maven_single_jar")
def _maven_impl(ctx):
  coords = []
  for mod in ctx.modules:
    coords += [dep.coord for dep in mod.tags.dep]
  output = ctx.execute(["coursier", "resolve", coords])  # hypothetical call
  repo_attrs = process_coursier(output)
  [maven_single_jar(**attrs) for attrs in repo_attrs]

در مثال بالا، تمام ماژول‌های گراف وابستگی ( ctx.modules ) را مرور می‌کنیم، که هر کدام یک شی bazel_module که فیلد tags همه تگ‌های maven maven.* روی ماژول را نشان می‌دهد. سپس از ابزار CLI Coursier برای تماس با Maven و انجام وضوح فراخوانی می کنیم. در نهایت، با استفاده از قانون فرضی maven_single_jar repo، از نتیجه وضوح برای ایجاد تعدادی مخزن استفاده می کنیم.