Mengelola dependensi eksternal dengan Bzlmod

Bzlmod adalah nama kode sistem dependensi eksternal baru yang diperkenalkan di Bazel 5.0. Fitur ini diperkenalkan untuk mengatasi beberapa poin kendala pada sistem lama yang tidak dapat diperbaiki secara bertahap; lihat bagian Pernyataan Masalah dalam dokumen desain asli untuk detail selengkapnya.

Pada Bazel 5.0, Bzlmod tidak diaktifkan secara default; flag --experimental_enable_bzlmod perlu ditentukan agar hal berikut dapat diterapkan. Seperti yang terlihat dari nama flag, fitur ini masih bersifat eksperimental; API dan perilaku dapat berubah hingga fitur ini diluncurkan secara resmi.

Untuk memigrasikan project Anda ke Bzlmod, ikuti Panduan Migrasi Bzlmod. Anda juga dapat menemukan contoh penggunaan Bzlmod di repositori contoh.

Modul Bazel

Sistem dependensi eksternal berbasis WORKSPACE lama dipusatkan di sekitar repositori (atau repo), yang dibuat melalui aturan repositori (atau aturan repositori). Meskipun repo masih merupakan konsep penting dalam sistem baru, modul adalah unit inti dependensi.

Modul pada dasarnya adalah project Bazel yang dapat memiliki beberapa versi, yang masing-masing memublikasikan metadata tentang modul lain yang menjadi dependensinya. Hal ini analog dengan konsep familier dalam sistem pengelolaan dependensi lainnya: artefak Maven, paket npm, peti Cargo, modul Go, dll.

Modul hanya menentukan dependensinya menggunakan pasangan name dan version, bukan URL spesifik di WORKSPACE. Dependensi tersebut kemudian dicari di registry Bazel; secara default, Bazel Central Registry. Di ruang kerja Anda, setiap modul kemudian diubah menjadi repo.

MODULE.bazel

Setiap versi setiap modul memiliki file MODULE.bazel yang mendeklarasikan dependensinya dan metadata lainnya. Berikut adalah contoh dasarnya:

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

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

File MODULE.bazel harus berada di root direktori ruang kerja (di samping file WORKSPACE). Tidak seperti file WORKSPACE, Anda tidak perlu menentukan dependensi transitif. Sebagai gantinya, Anda hanya boleh menentukan dependensi direct, dan file MODULE.bazel dependensi Anda akan diproses untuk menemukan dependensi transitif secara otomatis.

File MODULE.bazel mirip dengan file BUILD karena tidak mendukung bentuk alur kontrol apa pun; file ini juga melarang pernyataan load. Dukungan file perintah MODULE.bazel adalah:

Format versi

Bazel memiliki ekosistem yang beragam dan project menggunakan berbagai skema pembuatan versi. Yang paling populer sejauh ini adalah SemVer, tetapi ada juga project terkemuka yang menggunakan skema berbeda seperti Abseil, yang versinya berbasis tanggal, misalnya 20210324.2).

Karena alasan ini, Bzlmod mengadopsi versi spesifikasi SemVer yang lebih longgar. Perbedaannya meliputi:

  • SemVer menentukan bahwa bagian "rilis" dari versi harus terdiri dari 3 segmen: MAJOR.MINOR.PATCH. Di Bazel, persyaratan ini dilonggarkan sehingga sejumlah segmen diizinkan.
  • Di SemVer, tiap segmen dalam bagian "rilis" harus berupa digit saja. Di Bazel, hal ini dilonggarkan untuk memungkinkan huruf juga, dan semantik perbandingan cocok dengan "ID" di bagian "prarilis".
  • Selain itu, semantik peningkatan versi utama, minor, dan patch tidak diterapkan. (Namun, lihat tingkat kompatibilitas untuk mengetahui detail cara kami menunjukkan kompatibilitas mundur.)

Semua versi SemVer yang valid adalah versi modul Bazel yang valid. Selain itu, dua versi SemVer a dan b akan membandingkan a < b jika pembekuan yang sama saat dibandingkan dengan versi modul Bazel.

Resolusi versi

Masalah dependensi diamond adalah masalah pokok dalam ruang pengelolaan dependensi berversi. Misalkan Anda memiliki grafik dependensi berikut:

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

Versi D mana yang harus digunakan? Untuk mengatasi pertanyaan ini, Bzlmod menggunakan algoritma Minimal Version Selection (MVS) yang diperkenalkan dalam sistem modul Go. MVS berasumsi bahwa semua versi baru modul kompatibel dengan versi lama, sehingga hanya memilih versi tertinggi yang ditentukan oleh dependensi mana pun (D 1.1 dalam contoh kami). Disebut "minimal" karena D 1.1 di sini adalah versi minimal yang dapat memenuhi persyaratan kita; meskipun D 1.2 atau yang lebih baru ada, kita tidak memilihnya. Hal ini memiliki manfaat tambahan karena pemilihan versi-nya adalah high-fidelity dan dapat direproduksi.

Resolusi versi dilakukan secara lokal di komputer Anda, bukan oleh registry.

Tingkat kompatibilitas

Perhatikan bahwa asumsi MVS tentang kompatibilitas mundur dapat dilakukan karena hanya memperlakukan versi modul yang tidak kompatibel dengan versi sebelumnya sebagai modul terpisah. Dalam hal SemVer, itu berarti A 1.x dan A 2.x dianggap sebagai modul berbeda, dan dapat berdampingan dalam grafik dependensi yang di-resolve. Hal ini pada akhirnya dimungkinkan oleh fakta bahwa versi utama dienkode dalam jalur paket di Go, sehingga tidak ada konflik waktu kompilasi atau waktu penautan.

Di Bazel, kami tidak memiliki jaminan tersebut. Oleh karena itu, kita memerlukan cara untuk menunjukkan nomor "versi utama" agar dapat mendeteksi versi yang tidak kompatibel dengan versi sebelumnya. Angka ini disebut tingkat kompatibilitas, dan ditentukan oleh setiap versi modul dalam perintah module()-nya. Dengan informasi ini, kita dapat menampilkan error saat mendeteksi bahwa versi modul yang sama dengan tingkat kompatibilitas yang berbeda ada di grafik dependensi yang di-resolve.

Nama repositori

Di Bazel, setiap dependensi eksternal memiliki nama repositori. Terkadang, dependensi yang sama dapat digunakan melalui nama repositori yang berbeda (misalnya, @io_bazel_skylib dan @bazel_skylib berarti Bazel skylib), atau nama repositori yang sama dapat digunakan untuk dependensi yang berbeda dalam project yang berbeda.

Di Bzlmod, repositori dapat dibuat oleh modul Bazel dan ekstensi modul. Untuk mengatasi konflik nama repositori, kami menerapkan mekanisme pemetaan repositori dalam sistem baru. Berikut adalah dua konsep penting:

  • Nama repositori kanonis: Nama repositori unik secara global untuk setiap repositori. Ini akan menjadi nama direktori tempat repositori berada.
    Ini dibuat sebagai berikut (Peringatan: format nama kanonis bukan API yang harus Anda gunakan, format nama kanonis ini dapat berubah kapan saja):

    • Untuk repositori modul Bazel: module_name~version
      (Contoh. @bazel_skylib~1.0.3)
    • Untuk repositori ekstensi modul: module_name~version~extension_name~repo_name
      (Contoh. @rules_cc~0.0.1~cc_configure~local_config_cc)
  • Apparent repositori name: Nama repositori yang akan digunakan dalam file BUILD dan .bzl dalam repo. Dependensi yang sama dapat memiliki nama jelas yang berbeda dalam repo yang berbeda.
    Ditentukan sebagai berikut:

    • Untuk repositori modul Bazel: module_name secara default, atau nama yang ditentukan oleh atribut repo_name di bazel_dep.
    • Untuk repositori ekstensi modul: nama repositori yang diperkenalkan melalui use_repo.

Setiap repositori memiliki kamus pemetaan repositori dependensi langsungnya, yang merupakan peta dari nama repositori yang terlihat ke nama repositori kanonis. Kita menggunakan pemetaan repositori untuk me-resolve nama repositori saat membuat label. Perlu diperhatikan bahwa tidak ada konflik nama repositori kanonis, dan penggunaan nama repositori yang jelas dapat ditemukan dengan menguraikan file MODULE.bazel sehingga konflik dapat mudah diketahui dan diselesaikan tanpa memengaruhi dependensi lainnya.

dependensi Strict

Format spesifikasi dependensi baru memungkinkan kita melakukan pemeriksaan yang lebih ketat. Secara khusus, kami sekarang memberlakukan bahwa modul hanya dapat menggunakan repositori yang dibuat dari dependensi langsungnya. Hal ini membantu mencegah kerusakan yang tidak disengaja dan sulit di-debug saat sesuatu dalam grafik dependensi transitif berubah.

dependensi ketat diterapkan berdasarkan pemetaan repositori. Pada dasarnya, pemetaan repositori untuk setiap repo berisi semua dependensi langsung, sedangkan repositori lainnya tidak terlihat. Dependensi yang terlihat untuk setiap repositori ditentukan sebagai berikut:

  • Repositori modul Bazel dapat melihat semua repositori yang diperkenalkan dalam file MODULE.bazel melalui bazel_dep dan use_repo.
  • Repositori ekstensi modul dapat melihat semua dependensi yang terlihat dari modul yang menyediakan ekstensi, ditambah semua repo lain yang dihasilkan oleh ekstensi modul yang sama.

Registry

Bzlmod menemukan dependensi dengan meminta informasinya dari registri Bazel. Registry Bazel hanyalah database modul Bazel. Satu-satunya bentuk registry yang didukung adalah registry indeks, yang merupakan direktori lokal atau server HTTP statis dengan mengikuti format tertentu. Di masa mendatang, kami berencana menambahkan dukungan untuk registry modul tunggal, yang merupakan repo git yang berisi sumber dan histori project.

Registry indeks

Registry indeks adalah direktori lokal atau server HTTP statis yang berisi informasi tentang daftar modul, termasuk halaman beranda, pengelola, file MODULE.bazel setiap versi, dan cara mengambil sumber setiap versi. Secara khusus, server tidak perlu menyajikan arsip sumber itu sendiri.

Registry indeks harus mengikuti format di bawah:

  • /bazel_registry.json: File JSON yang berisi metadata untuk registry seperti:
    • mirrors, menentukan daftar mirror yang akan digunakan untuk arsip sumber.
    • module_base_path, yang menentukan jalur dasar untuk modul dengan jenis local_repository dalam file source.json.
  • /modules: Direktori yang berisi subdirektori untuk setiap modul dalam registry ini.
  • /modules/$MODULE: Direktori yang berisi subdirektori untuk setiap versi modul ini, serta file berikut:
    • metadata.json: File JSON yang berisi informasi tentang modul, dengan kolom berikut:
      • homepage: URL halaman beranda project.
      • maintainers: Daftar objek JSON, yang masing-masing sesuai dengan informasi pengelola modul dalam registry. Perhatikan bahwa ini tidak selalu sama dengan penulis project.
      • versions: Daftar semua versi modul ini yang dapat ditemukan di registry ini.
      • yanked_versions: Daftar versi yang dicabut dari modul ini. Ini saat ini tanpa pengoperasian, tetapi di masa mendatang, versi yang ditarik akan dilewati atau menghasilkan error.
  • /modules/$MODULE/$VERSION: Direktori yang berisi file berikut:
    • MODULE.bazel: File MODULE.bazel versi modul ini.
    • source.json: File JSON yang berisi informasi tentang cara mengambil sumber versi modul ini.
      • Jenis defaultnya adalah "arsip" dengan kolom berikut:
        • url: URL arsip sumber.
        • integrity: Checksum Subresource Integrity untuk arsip.
        • strip_prefix: Awalan direktori ke strip saat mengekstrak arsip sumber.
        • patches: Daftar string, yang masing-masing menyebutkan file patch untuk diterapkan ke arsip yang diekstrak. File patch berada di direktori /modules/$MODULE/$VERSION/patches.
        • patch_strip: Sama seperti argumen --strip pada patch Unix.
      • Jenis dapat diubah untuk menggunakan jalur lokal dengan kolom berikut:
        • type: local_path
        • path: Jalur lokal ke repo, yang dihitung sebagai berikut:
          • Jika jalur adalah jalur absolut, akan digunakan sebagaimana adanya.
          • Jika jalur adalah jalur relatif dan module_base_path adalah jalur absolut, jalur akan di-resolve menjadi <module_base_path>/<path>
          • Jika jalur dan module_base_path merupakan jalur relatif, jalur akan diselesaikan menjadi <registry_path>/<module_base_path>/<path>. Registry harus dihosting secara lokal dan digunakan oleh --registry=file://<registry_path>. Jika tidak, Bazel akan menampilkan error.
    • patches/: Direktori opsional yang berisi file patch, hanya digunakan jika source.json memiliki jenis "arsip".

Registry Pusat Bazel

Bazel Central Registry (BCR) adalah registry indeks yang berlokasi di bcr.bazel.build. Kontennya didukung oleh repo GitHub bazelbuild/bazel-central-registry.

BCR dikelola oleh komunitas Bazel; kontributor dipersilakan untuk mengirimkan permintaan pull. Lihat Kebijakan dan Prosedur Bazel Central Registry.

Selain mengikuti format registry indeks normal, BCR memerlukan file presubmit.yml untuk setiap versi modul (/modules/$MODULE/$VERSION/presubmit.yml). File ini menentukan beberapa target build dan pengujian penting yang dapat digunakan untuk memeriksa validitas versi modul ini, dan digunakan oleh pipeline CI BCR untuk memastikan interoperabilitas antar-modul dalam BCR.

Memilih registry

Flag Bazel berulang --registry dapat digunakan untuk menentukan daftar registri yang akan diminta modulnya, sehingga Anda dapat menyiapkan project untuk mengambil dependensi dari registry pihak ketiga atau internal. Registry sebelumnya lebih diutamakan. Untuk memudahkan, Anda dapat menempatkan daftar flag --registry dalam file .bazelrc project Anda.

Ekstensi Modul

Ekstensi modul memungkinkan Anda memperluas sistem modul dengan membaca data input dari modul di seluruh grafik dependensi, menjalankan logika yang diperlukan untuk menyelesaikan dependensi, dan terakhir membuat repo dengan memanggil aturan repo. Fungsinya mirip dengan makro WORKSPACE saat ini, tetapi lebih cocok dalam hal modul dan dependensi transitif.

Ekstensi modul ditentukan dalam file .bzl, seperti aturan repo atau makro WORKSPACE. Pengujian ini tidak dipanggil secara langsung; tetapi setiap modul dapat menentukan bagian data yang disebut tag agar ekstensi akan dibaca. Kemudian, setelah resolusi versi modul selesai, ekstensi modul akan dijalankan. Setiap ekstensi dijalankan satu kali setelah penyelesaian modul (masih sebelum build apa pun benar-benar terjadi), dan mulai membaca semua tag yang dimilikinya di seluruh grafik dependensi.

          [ 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) ]

Pada contoh grafik dependensi di atas, A 1.1 dan B 1.2, dll. adalah modul Bazel; Anda dapat menganggap masing-masingnya sebagai file MODULE.bazel. Setiap modul dapat menentukan beberapa tag untuk ekstensi modul; di sini beberapa ditentukan untuk ekstensi "maven", dan beberapa ditentukan untuk "cargo". Saat grafik dependensi ini selesai (misalnya, mungkin B 1.2 sebenarnya memiliki bazel_dep di D 1.3 tetapi diupgrade ke D 1.4 karena C), ekstensi "maven" akan dijalankan, dan dapat membaca semua tag maven.*, menggunakan informasi di dalamnya untuk menentukan repositori mana yang akan dibuat. Demikian pula untuk ekstensi "kargo".

Penggunaan ekstensi

Ekstensi dihosting dalam modul Bazel sendiri, jadi untuk menggunakan ekstensi dalam modul, Anda harus menambahkan bazel_dep pada modul tersebut terlebih dahulu, lalu memanggil fungsi bawaan use_extension agar dapat memasukkannya ke dalam cakupan. Pertimbangkan contoh berikut, cuplikan dari file MODULE.bazel untuk menggunakan ekstensi "maven" fiktif yang ditentukan dalam modul rules_jvm_external:

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

Setelah memasukkan ekstensi ke dalam cakupan, Anda dapat menggunakan sintaksis titik untuk menentukan tag untuk ekstensi tersebut. Perhatikan bahwa tag harus mengikuti skema yang ditentukan oleh class tag yang sesuai (lihat definisi ekstensi di bawah). Berikut adalah contoh yang menentukan beberapa tag maven.dep dan 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")

Jika ekstensi menghasilkan repo yang ingin Anda gunakan dalam modul, gunakan perintah use_repo untuk mendeklarasikannya. Hal ini dilakukan untuk memenuhi kondisi dependensi yang ketat dan menghindari konflik nama repo lokal.

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

Repositori yang dihasilkan oleh ekstensi adalah bagian dari API-nya. Jadi, dari tag yang Anda tentukan, Anda harus tahu bahwa ekstensi "maven" akan menghasilkan repositori yang disebut "org_junit_junit", dan yang disebut "com_google_guava_guava". Dengan use_repo, Anda dapat memilih untuk mengganti namanya dalam cakupan modul, misalnya "guava" di sini.

Definisi ekstensi

Ekstensi modul ditentukan dengan cara yang mirip dengan aturan repo, menggunakan fungsi module_extension. Keduanya memiliki fungsi implementasi; tetapi meskipun aturan repo memiliki sejumlah atribut, ekstensi modul memiliki sejumlah tag_class yang masing-masing memiliki sejumlah atribut. Class tag menentukan skema untuk tag yang digunakan oleh ekstensi ini. Melanjutkan contoh ekstensi "maven" hipotetis di atas:

# @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},
)

Deklarasi ini menjelaskan bahwa tag maven.dep dan maven.pom dapat ditentukan, menggunakan skema atribut yang didefinisikan di atas.

Fungsi penerapannya mirip dengan makro WORKSPACE, tetapi hal ini mendapatkan objek module_ctx, yang memberikan akses ke grafik dependensi dan semua tag terkait. Fungsi penerapan kemudian harus memanggil aturan repo untuk membuat repositori:

# @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]

Pada contoh di atas, kita membahas semua modul dalam grafik dependensi (ctx.modules), yang masing-masing merupakan objek bazel_module dengan kolom tags yang mengekspos semua tag maven.* pada modul. Kemudian, kita memanggil utilitas CLI Coursier untuk menghubungi Maven dan melakukan resolusi. Terakhir, kita menggunakan hasil resolusi untuk membuat sejumlah repositori, menggunakan aturan repositori maven_single_jar hipotetis.