Codebase Bazel

Laporkan masalah Lihat sumber

Dokumen ini adalah deskripsi codebase dan cara Bazel disusun. Fitur ini ditujukan bagi orang yang bersedia berkontribusi pada Bazel, bukan untuk pengguna akhir.

Pengantar

Codebase Bazel berukuran besar (~350KLOC kode produksi dan ~260 kode uji KLOC) dan tidak ada yang familier dengan keseluruhan lanskap: semua orang sangat mengetahui lembah tertentu mereka, tetapi hanya sedikit yang tahu apa yang ada di atas bukit di setiap arah.

Agar orang-orang di tengah perjalanan tidak mendapati diri mereka berada dalam gelap hutan dengan jalur langsung yang hilang, dokumen ini mencoba memberikan ringkasan codebase sehingga lebih mudah untuk mulai mengerjakannya.

Versi publik kode sumber Bazel tersedia di GitHub di github.com/bazelbuild/bazel. Ini bukanlah "sumber kebenaran"; ini berasal dari hierarki sumber internal Google yang berisi fungsi tambahan yang tidak berguna di luar Google. Tujuan jangka panjangnya adalah menjadikan GitHub sebagai sumber kebenaran.

Kontribusi diterima melalui mekanisme permintaan pull GitHub reguler, dan diimpor secara manual oleh Googler ke hierarki sumber internal, lalu diekspor kembali ke GitHub.

Arsitektur klien/server

Sebagian besar Bazel berada dalam proses server yang tetap berada di dalam RAM antar-build. Hal ini memungkinkan Bazel mempertahankan status di antara build.

Inilah alasan command line Bazel memiliki dua jenis opsi: startup dan perintah. Di command line seperti ini:

    bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar

Beberapa opsi (--host_jvm_args=) diletakkan sebelum nama perintah yang akan dijalankan dan beberapa lainnya diletakkan setelah (-c opt); jenis pertama disebut "opsi startup" dan memengaruhi proses server secara keseluruhan, sedangkan jenis yang kedua, "opsi perintah", hanya memengaruhi satu perintah.

Setiap instance server memiliki satu ruang kerja terkait (kumpulan pohon sumber yang dikenal sebagai "repositori") dan setiap ruang kerja biasanya memiliki satu instance server aktif. Hal ini dapat diatasi dengan menentukan basis output kustom (lihat bagian "Tata letak direktori" untuk informasi selengkapnya).

Bazel didistribusikan sebagai satu file ELF yang dapat dieksekusi yang juga berupa file .zip yang valid. Saat Anda mengetik bazel, file ELF yang dapat dieksekusi di atas yang diimplementasikan dalam C++ ("klien") akan mendapatkan kontrol. Tutorial ini menyiapkan proses server yang sesuai menggunakan langkah-langkah berikut:

  1. Memeriksa apakah kode sudah mengekstrak dengan sendirinya. Jika tidak, sistem akan melakukannya. Dari sinilah implementasi server berasal.
  2. Memeriksa apakah ada instance server aktif yang berfungsi: instance sedang berjalan, memiliki opsi startup yang tepat, dan menggunakan direktori ruang kerja yang tepat. Fitur ini menemukan server yang sedang berjalan dengan melihat direktori $OUTPUT_BASE/server yang terdapat file kunci dengan port yang diproses server.
  3. Jika perlu, hentikan proses server lama
  4. Jika perlu, mulai proses server baru

Setelah proses server yang sesuai siap, perintah yang perlu dijalankan akan dikomunikasikan melalui antarmuka gRPC, lalu output Bazel akan disalurkan kembali ke terminal. Hanya satu perintah yang dapat dijalankan secara bersamaan. Hal ini diimplementasikan menggunakan mekanisme penguncian yang rumit dengan beberapa bagian di C++ dan bagian-bagian di Java. Ada beberapa infrastruktur untuk menjalankan beberapa perintah secara paralel, karena ketidakmampuan untuk menjalankan bazel version secara paralel dengan perintah lain akan agak memalukan. Pemblokir utama adalah siklus proses BlazeModule dan beberapa status di BlazeRuntime.

Di akhir perintah, server Bazel akan mengirimkan kode keluar yang harus ditampilkan klien. Kerugian yang menarik adalah implementasi bazel run: tugas perintah ini adalah menjalankan sesuatu yang baru saja dibuat Bazel, tetapi tidak dapat melakukannya dari proses server karena tidak memiliki terminal. Jadi, kode ini memberi tahu klien biner apa yang harus ujexec() dan dengan argumen apa.

Saat seseorang menekan Ctrl-C, klien menerjemahkannya menjadi panggilan Cancel pada koneksi gRPC, yang mencoba menghentikan perintah sesegera mungkin. Setelah Ctrl-C ketiga, klien mengirimkan SIGKILL ke server.

Kode sumber klien berada di src/main/cpp dan protokol yang digunakan untuk berkomunikasi dengan server ada di src/main/protobuf/command_server.proto .

Titik entri utama server adalah BlazeRuntime.main() dan panggilan gRPC dari klien ditangani oleh GrpcServerImpl.run().

Tata letak direktori

Bazel membuat set direktori yang agak rumit selama proses build. Deskripsi lengkap tersedia di Tata letak direktori output.

"Repo utama" adalah pohon sumber tempat Bazel dijalankan. Biasanya sesuai dengan sesuatu yang Anda periksa dari kontrol sumber. Root direktori ini disebut "root ruang kerja".

Bazel menempatkan semua datanya di bawah "root user output". Ini biasanya $HOME/.cache/bazel/_bazel_${USER}, tetapi dapat diganti menggunakan opsi startup --output_user_root.

"Basis penginstalan" adalah tempat Bazel diekstrak. Hal ini dilakukan secara otomatis dan setiap versi Bazel mendapatkan subdirektori berdasarkan checksum di bawah basis penginstalan. Atribut ini berada di $OUTPUT_USER_ROOT/install secara default dan dapat diubah menggunakan opsi command line --install_base.

"Basis output" adalah tempat instance Bazel terpasang ke ruang kerja tertentu yang digunakan untuk menulis. Setiap basis output memiliki maksimal satu instance server Bazel yang berjalan kapan saja. Biasanya pukul $OUTPUT_USER_ROOT/<checksum of the path to the workspace>. Ini dapat diubah menggunakan opsi startup --output_base, yang, antara lain, berguna untuk mengatasi batasan bahwa hanya satu instance Bazel yang dapat berjalan di ruang kerja mana pun pada waktu tertentu.

Direktori output berisi, antara lain:

  • Repositori eksternal yang diambil di $OUTPUT_BASE/external.
  • Root exec, direktori yang berisi symlink ke semua kode sumber untuk build saat ini. Lokasinya di $OUTPUT_BASE/execroot. Selama proses build, direktori kerjanya adalah $EXECROOT/<name of main repository>. Kami berencana mengubahnya menjadi $EXECROOT, meskipun ini merupakan paket jangka panjang karena merupakan perubahan yang sangat tidak kompatibel.
  • File yang dibuat selama build.

Proses menjalankan perintah

Setelah server Bazel mendapatkan kontrol dan diberi tahu tentang perintah yang diperlukan untuk dijalankan, urutan peristiwa berikut akan terjadi:

  1. BlazeCommandDispatcher akan diberi tahu tentang permintaan baru tersebut. File ini menentukan apakah perintah memerlukan ruang kerja untuk dijalankan (hampir setiap perintah kecuali perintah yang tidak ada hubungannya dengan kode sumber, misalnya versi atau bantuan) dan apakah perintah lain sedang berjalan.

  2. Perintah yang tepat ditemukan. Setiap perintah harus menerapkan antarmuka BlazeCommand dan harus memiliki anotasi @Command (ini agak antipola, akan lebih baik jika semua metadata yang dibutuhkan perintah dijelaskan dengan metode di BlazeCommand)

  3. Opsi command line diuraikan. Setiap perintah memiliki opsi command line yang berbeda, yang dijelaskan dalam anotasi @Command.

  4. Bus acara dibuat. Bus peristiwa adalah aliran data untuk peristiwa yang terjadi selama build. Beberapa di antaranya diekspor ke luar Bazel dengan aegis Build Event Protocol untuk memberi tahu dunia tentang cara kerja build.

  5. Perintah mendapatkan kontrol. Perintah yang paling menarik adalah yang menjalankan build: build, uji, jalankan, cakupan, dan seterusnya: fungsi ini diimplementasikan oleh BuildTool.

  6. Rangkaian pola target pada command line diuraikan dan karakter pengganti seperti //pkg:all dan //pkg/... akan diselesaikan. Hal ini diterapkan di AnalysisPhaseRunner.evaluateTargetPatterns() dan direifikasi di Skyframe sebagai TargetPatternPhaseValue.

  7. Fase pemuatan/analisis dijalankan untuk menghasilkan grafik tindakan (grafik acyclic terarah perintah yang perlu dijalankan untuk build).

  8. Fase eksekusi akan dijalankan. Artinya, setiap tindakan yang diperlukan untuk membuat target level teratas yang diminta akan dijalankan.

Opsi command line

Opsi command line untuk pemanggilan Bazel dijelaskan dalam objek OptionsParsingResult, yang kemudian berisi peta dari "class opsi" ke nilai opsi. "Class opsi" adalah subclass OptionsBase dan mengelompokkan opsi command line bersama-sama yang saling terkait. Contoh:

  1. Opsi yang terkait dengan bahasa pemrograman (CppOptions atau JavaOptions). Opsi ini harus berupa subclass dari FragmentOptions dan pada akhirnya digabungkan ke dalam objek BuildOptions.
  2. Opsi yang terkait dengan cara Bazel mengeksekusi tindakan (ExecutionOptions)

Opsi ini dirancang untuk digunakan dalam fase analisis dan (baik melalui RuleContext.getFragment() di Java maupun ctx.fragments di Starlark). Beberapa di antaranya (misalnya, apakah akan melakukan C++ dengan menyertakan pemindaian atau tidak) dibaca dalam fase eksekusi, tetapi proses tersebut selalu memerlukan plumbing eksplisit karena BuildConfiguration tidak tersedia pada saat itu. Untuk mengetahui informasi selengkapnya, lihat bagian "Konfigurasi".

PERINGATAN: Kami ingin berpura-pura bahwa instance OptionsBase tidak dapat diubah dan menggunakannya seperti itu (seperti bagian dari SkyKeys). Padahal, tidak demikian dan mengubahnya adalah cara yang sangat bagus untuk merusak Bazel dengan cara yang samar dan sulit di-debug. Sayangnya, upaya untuk menjadikannya tidak dapat diubah merupakan upaya yang besar. (Anda dapat memodifikasi FragmentOptions segera setelah pembuatan sebelum orang lain mendapatkan kesempatan untuk menyimpan referensi ke sana dan sebelum equals() atau hashCode() dipanggil.)

Bazel mempelajari class opsi dengan cara berikut:

  1. Beberapa di antaranya terhubung ke Bazel (CommonCommandOptions)
  2. Dari anotasi @Command di setiap perintah Bazel
  3. Dari ConfiguredRuleClassProvider (ini adalah opsi command line yang terkait dengan bahasa pemrograman individual)
  4. Aturan Starlark juga dapat menentukan opsinya sendiri (lihat di sini)

Setiap opsi (kecuali opsi yang ditentukan Starlark) adalah variabel anggota subclass FragmentOptions yang memiliki anotasi @Option, yang menentukan nama dan jenis opsi command line beserta beberapa teks bantuan.

Jenis nilai opsi command line Java biasanya adalah sesuatu yang sederhana (string, bilangan bulat, Boolean, label, dll.). Namun, kami juga mendukung opsi jenis yang lebih rumit; dalam hal ini, tugas konversi dari string command line menjadi jenis data adalah pada implementasi com.google.devtools.common.options.Converter.

Pohon sumber, seperti yang dilihat oleh Bazel

Bazel berada di bisnis pembuatan software, yang terjadi dengan membaca dan menafsirkan kode sumber. Totalitas kode sumber yang dioperasikan Bazel disebut "ruang kerja" dan disusun menjadi repositori, paket, serta aturan.

Repositori

"Repositori" adalah hierarki sumber tempat developer bekerja; biasanya mewakili satu project. Ancestor Bazel, Blaze, beroperasi pada monorepo, yaitu satu pohon sumber yang berisi semua kode sumber yang digunakan untuk menjalankan build. Sebaliknya, Bazel mendukung project yang kode sumbernya mencakup beberapa repositori. Repositori tempat Bazel dipanggil disebut "repositori utama", yang lainnya disebut "repositori eksternal".

Repositori ditandai dengan file batas repo (MODULE.bazel, REPO.bazel, atau dalam konteks lama, WORKSPACE atau WORKSPACE.bazel) di direktori root-nya. Repositori utama adalah hierarki sumber tempat Anda memanggil Bazel. Repositori eksternal ditentukan dengan berbagai cara; lihat ringkasan dependensi eksternal untuk informasi selengkapnya.

Kode repositori eksternal di-symlink atau didownload di $OUTPUT_BASE/external.

Saat menjalankan build, seluruh hierarki sumber harus digabungkan; ini dilakukan oleh SymlinkForest, yang membuat symlink setiap paket dalam repositori utama ke $EXECROOT dan setiap repositori eksternal ke $EXECROOT/external atau $EXECROOT/...

Paket

Setiap repositori terdiri dari paket, kumpulan file terkait, dan spesifikasi dependensi. Class ini ditentukan oleh file bernama BUILD atau BUILD.bazel. Jika keduanya ada, Bazel akan lebih memilih BUILD.bazel; alasan file BUILD masih diterima adalah ancestor Bazel, Blaze, menggunakan nama file ini. Namun, segmen tersebut ternyata merupakan segmen jalur yang umum digunakan, terutama di Windows, yang nama filenya tidak peka huruf besar/kecil.

Paket tidak bergantung satu sama lain: perubahan pada file BUILD dari suatu paket tidak dapat menyebabkan paket lain berubah. Penambahan atau penghapusan file BUILD _dapat _mengubah paket lain karena glob rekursif berhenti pada batas paket sehingga keberadaan file BUILD akan menghentikan rekursi.

Evaluasi file BUILD disebut "package memuat". Ini diimplementasikan di class PackageFactory, bekerja dengan memanggil penafsir Starlark, dan memerlukan pengetahuan tentang kumpulan class aturan yang tersedia. Hasil pemuatan paket adalah objek Package. Umumnya, ini adalah peta dari string (nama target) ke target itu sendiri.

Sebagian besar kompleksitas selama pemuatan paket adalah globbing: Bazel tidak memerlukan setiap file sumber dicantumkan secara eksplisit dan sebagai gantinya dapat menjalankan glob (seperti glob(["**/*.java"])). Tidak seperti shell, shell mendukung glob berulang yang turun ke subdirektori (tetapi tidak ke dalam sub-paket). Hal ini memerlukan akses ke sistem file dan karena prosesnya bisa berjalan lambat, kami menerapkan segala macam trik untuk membuatnya berjalan secara paralel dan seefisien mungkin.

Globbing diterapkan di class berikut:

  • LegacyGlobber, globber yang tidak peka dan cepat dari Skyframe
  • SkyframeHybridGlobber, versi yang menggunakan Skyframe dan kembali ke globber lama untuk menghindari "Skyframe dimulai ulang" (dijelaskan di bawah)

Class Package sendiri berisi beberapa anggota yang secara eksklusif digunakan untuk mengurai paket "eksternal" (terkait dengan dependensi eksternal) dan yang tidak masuk akal untuk paket sebenarnya. Ini adalah cacat desain karena objek yang mendeskripsikan paket reguler tidak boleh berisi kolom yang menjelaskan hal lain. Fitur tersebut meliputi:

  • Pemetaan repositori
  • Toolchain terdaftar
  • Platform eksekusi terdaftar

Idealnya, akan ada lebih banyak pemisahan antara mengurai paket "eksternal" dari mengurai paket reguler sehingga Package tidak perlu memenuhi kebutuhan keduanya. Sayangnya, hal ini sulit dilakukan karena keduanya terikat cukup dalam.

Label, Target, dan Aturan

Paket terdiri dari target, yang memiliki jenis berikut:

  1. File: berbagai hal yang merupakan input atau output build. Dalam istilah Bazel, kami menyebutnya artefak (dibahas di tempat lain). Tidak semua file yang dibuat selama build adalah target; biasanya output Bazel tidak memiliki label terkait.
  2. Aturan: aturan ini menjelaskan langkah-langkah untuk mendapatkan output dari inputnya. Library ini umumnya terkait dengan bahasa pemrograman (seperti cc_library, java_library, atau py_library), tetapi ada beberapa bahasa tanpa bahasa (seperti genrule atau filegroup)
  3. Grup paket: dibahas di bagian Visibilitas.

Nama target disebut Label. Sintaksis labelnya adalah @repo//pac/kage:name, dengan repo adalah nama repositori tempat Label berada, pac/kage adalah direktori tempat file BUILD-nya berada, dan name adalah jalur file (jika label merujuk ke file sumber) yang relatif terhadap direktori paket. Saat merujuk ke target pada command line, beberapa bagian label dapat dihilangkan:

  1. Jika repositori dihilangkan, label akan diambil untuk berada di repositori utama.
  2. Jika bagian paket dihilangkan (seperti name atau :name), label dianggap berada dalam paket direktori kerja saat ini (jalur relatif berisi referensi level lebih tinggi (..) tidak diizinkan)

Semacam aturan (seperti "library C++") disebut "class aturan". Class aturan dapat diterapkan di Starlark (fungsi rule()) atau di Java (disebut dengan "aturan native", dengan jenis RuleClass). Dalam jangka panjang, setiap aturan khusus bahasa akan diterapkan di Starlark, tetapi beberapa kelompok aturan lama (seperti Java atau C++) masih ada di Java untuk saat ini.

Class aturan Starlark harus diimpor di awal file BUILD menggunakan pernyataan load(), sedangkan class aturan Java "secara bawaan" dikenal oleh Bazel, karena telah terdaftar di ConfiguredRuleClassProvider.

Class aturan berisi informasi seperti:

  1. Atributnya (seperti srcs, deps): jenisnya, nilai default, batasannya, dll.
  2. Transisi dan aspek konfigurasi yang dilampirkan pada setiap atribut, jika ada
  3. Implementasi aturan
  4. Aturan "biasanya" dibuat oleh penyedia info transitif

Catatan terminologi: Di codebase, kami sering menggunakan "Aturan" untuk berarti target yang dibuat oleh class aturan. Namun dalam Starlark dan dalam dokumentasi yang ditampilkan kepada pengguna, "Aturan" harus digunakan secara eksklusif untuk merujuk ke class aturan itu sendiri; targetnya hanyalah "target". Perhatikan juga bahwa meskipun RuleClass memiliki "class" dalam namanya, tidak ada hubungan pewarisan Java antara class aturan dan target jenis tersebut.

Bingkai Ruang Angkasa

Framework evaluasi yang mendasari Bazel disebut Skyframe. Modelnya adalah semua hal yang perlu dibangun selama build akan diatur menjadi grafik asiklik terarah dengan tepi yang mengarah dari bagian data apa pun ke dependensinya, yaitu, bagian data lain yang perlu diketahui untuk membangunnya.

Node dalam grafik disebut SkyValue dan namanya disebut SkyKey. Keduanya sangat tidak dapat diubah; hanya objek yang tidak dapat diubah yang harus dapat dijangkau dari keduanya. Invarian ini hampir selalu berlaku, dan jika tidak (misalnya untuk class opsi individual BuildOptions, yang merupakan anggota BuildConfigurationValue dan SkyKey-nya), kami berusaha sangat keras untuk tidak mengubahnya atau mengubahnya dengan cara yang tidak dapat diamati dari luar. Oleh karena itu, semua yang dihitung dalam Skyframe (seperti target yang dikonfigurasi) juga tidak dapat diubah.

Cara paling mudah untuk mengamati grafik Skyframe adalah dengan menjalankan bazel dump --skyframe=deps, yang mengeluarkan grafik, satu SkyValue per baris. Sebaiknya lakukan ini untuk build kecil, karena ukurannya bisa menjadi cukup besar.

Skyframe berada dalam paket com.google.devtools.build.skyframe. Paket bernama serupa com.google.devtools.build.lib.skyframe berisi implementasi Bazel di atas Skyframe. Informasi selengkapnya tentang Skyframe tersedia di sini.

Untuk mengevaluasi SkyKey yang ditentukan ke dalam SkyValue, Skyframe akan memanggil SkyFunction yang sesuai dengan jenis kunci. Selama evaluasi fungsi, fungsi tersebut mungkin meminta dependensi lain dari Skyframe dengan memanggil berbagai overload SkyFunction.Environment.getValue(). Hal ini memiliki efek samping pendaftaran dependensi tersebut ke grafik internal Skyframe, sehingga Skyframe akan tahu untuk mengevaluasi kembali fungsi tersebut saat dependensinya berubah. Dengan kata lain, caching dan komputasi inkremental Skyframe bekerja pada tingkat perincian SkyFunction dan SkyValue.

Setiap kali SkyFunction meminta dependensi yang tidak tersedia, getValue() akan menampilkan null. Fungsi ini kemudian akan menghasilkan kontrol kembali ke Skyframe dengan sendiri menampilkan null. Di kemudian hari, Skyframe akan mengevaluasi dependensi yang tidak tersedia, lalu memulai ulang fungsi tersebut dari awal — hanya kali ini panggilan getValue() yang akan berhasil dengan hasil non-null.

Konsekuensinya, komputasi apa pun yang dilakukan di dalam SkyFunction sebelum mulai ulang harus diulang. Namun, hal ini tidak termasuk pekerjaan yang dilakukan untuk mengevaluasi dependensi SkyValues, yang di-cache. Oleh karena itu, kami biasanya mengatasi masalah ini dengan:

  1. Mendeklarasikan dependensi dalam batch (dengan menggunakan getValuesAndExceptions()) untuk membatasi jumlah mulai ulang.
  2. Memecah SkyValue menjadi bagian-bagian terpisah yang dihitung oleh SkyFunction yang berbeda, sehingga dapat dikomputasi dan di-cache secara terpisah. Hal ini harus dilakukan secara strategis, karena berpotensi meningkatkan penggunaan memori.
  3. Menyimpan status di antara proses mulai ulang, baik menggunakan SkyFunction.Environment.getState(), maupun mempertahankan cache statis ad hoc "di belakang Skyframe". Dengan SkyFunction yang kompleks, pengelolaan status antara proses mulai ulang dapat menjadi rumit, sehingga StateMachine diperkenalkan untuk pendekatan terstruktur ke konkurensi logis, termasuk hook untuk menangguhkan dan melanjutkan komputasi hierarkis dalam SkyFunction. Contoh: DependencyResolver#computeDependencies menggunakan StateMachine dengan getState() untuk menghitung kemungkinan serangkaian dependensi langsung yang sangat besar dari target yang dikonfigurasi, yang jika tidak, dapat mengakibatkan mulai ulang yang mahal.

Pada dasarnya, Bazel memerlukan jenis solusi ini karena ratusan ribu node Skyframe yang sedang beroperasi merupakan hal yang umum, dan dukungan Java untuk thread ringan tidak mengungguli implementasi StateMachine sejak tahun 2023.

Starlark

Starlark adalah bahasa khusus domain yang digunakan pengguna untuk mengonfigurasi dan memperluas Bazel. Fungsi ini dirancang sebagai subset Python terbatas yang memiliki jenis yang jauh lebih sedikit, lebih banyak batasan pada alur kontrol, dan yang paling penting, jaminan ketetapan yang kuat untuk mengaktifkan pembacaan serentak. Belum selesai menggunakan Turing, yang membuat beberapa pengguna (tetapi tidak semua) enggan mencoba menyelesaikan tugas pemrograman umum dalam bahasa.

Starlark diimplementasikan dalam paket net.starlark.java. Platform ini juga memiliki implementasi Go independen di sini. Implementasi Java yang digunakan di Bazel saat ini merupakan penafsir.

Starlark digunakan dalam beberapa konteks, termasuk:

  1. File BUILD. Di sinilah target build baru akan ditentukan. Kode Starlark yang berjalan dalam konteks ini hanya memiliki akses ke konten file BUILD itu sendiri dan file .bzl yang dimuat olehnya.
  2. File MODULE.bazel. Di sinilah dependensi eksternal ditentukan. Kode Starlark yang berjalan dalam konteks ini hanya memiliki akses yang sangat terbatas ke beberapa perintah yang telah ditetapkan.
  3. File .bzl. Di sinilah aturan build baru, aturan repo, dan ekstensi modul ditentukan. Kode Starlark di sini dapat menentukan fungsi baru dan memuat dari file .bzl lainnya.

Dialek yang tersedia untuk file BUILD dan .bzl sedikit berbeda karena keduanya mengekspresikan hal yang berbeda. Daftar perbedaannya tersedia di sini.

Informasi selengkapnya tentang Starlark tersedia di sini.

Fase pemuatan/analisis

Fase pemuatan/analisis adalah saat Bazel menentukan tindakan yang diperlukan untuk membuat aturan tertentu. Unit dasarnya adalah "target yang dikonfigurasi", yang cukup masuk akal, merupakan pasangan (target, konfigurasi).

Ini disebut "fase pemuatan/analisis" karena dapat dibagi menjadi dua bagian yang berbeda, yang dulunya diserialisasi, tetapi kini dapat saling tumpang tindih pada waktunya:

  1. Memuat paket, yaitu mengubah file BUILD menjadi objek Package yang mewakilinya
  2. Menganalisis target yang dikonfigurasi, yaitu menjalankan implementasi aturan untuk menghasilkan grafik tindakan

Setiap target yang dikonfigurasi dalam penutupan transitif target yang dikonfigurasi yang diminta pada command line harus dianalisis dari bawah ke atas. Artinya, node daun terlebih dahulu, lalu hingga yang ada di command line. Input untuk analisis satu target yang dikonfigurasi adalah:

  1. Konfigurasi. ("cara" membuat aturan tersebut; misalnya, platform target dan juga hal-hal seperti opsi command line yang ingin diteruskan ke compiler C++)
  2. Dependensi langsung. Penyedia info transitif mereka tersedia untuk aturan yang dianalisis. Fungsi ini disebut demikian karena memberikan "roll-up" informasi dalam penutupan transitif target yang dikonfigurasi, seperti semua file .jar di classpath atau semua file .o yang perlu ditautkan ke biner C++)
  3. Target itu sendiri. Ini adalah hasil dari pemuatan paket tempat target berada. Untuk aturan, ini mencakup atributnya, yang biasanya merupakan hal penting.
  4. Implementasi target yang dikonfigurasi. Untuk aturan, ini bisa berupa Starlark atau Java. Semua target yang tidak dikonfigurasi dengan aturan diterapkan di Java.

Output dari menganalisis target yang dikonfigurasi adalah:

  1. Penyedia info transitif yang mengonfigurasi target yang bergantung padanya dapat mengakses
  2. Artefak yang dapat dibuat dan tindakan yang menghasilkannya.

API yang ditawarkan ke aturan Java adalah RuleContext, yang setara dengan argumen ctx dari aturan Starlark. API-nya lebih andal, tetapi pada saat yang sama, menjadi lebih mudah melakukan Bad ThingsTM. Misalnya, untuk menulis kode yang waktu atau kompleksitasnya bersifat kuadrat (atau lebih buruk), untuk membuat server Bazel error dengan pengecualian Java atau melanggar invarian (seperti dengan tidak sengaja memodifikasi instance Options atau dengan membuat target yang dikonfigurasi dapat berubah)

Algoritma yang menentukan dependensi langsung dari target yang dikonfigurasi berada di DependencyResolver.dependentNodeMap().

Konfigurasi

Konfigurasi adalah "bagaimana" membangun target: untuk platform apa, dengan opsi command line apa, dll.

Target yang sama bisa dibuat untuk beberapa konfigurasi dalam build yang sama. Hal ini berguna, misalnya, saat kode yang sama digunakan untuk alat yang dijalankan selama proses build serta untuk kode target, dan kita melakukan kompilasi silang atau saat mem-build aplikasi Android yang populer (aplikasi yang berisi kode native untuk beberapa arsitektur CPU)

Secara konseptual, konfigurasinya adalah instance BuildOptions. Namun, pada praktiknya, BuildOptions digabungkan oleh BuildConfiguration yang menyediakan beragam fungsi tambahan. Hal ini menyebar dari bagian atas grafik dependensi ke bagian bawah. Jika berubah, build perlu dianalisis ulang.

Hal ini menyebabkan anomali seperti harus menganalisis ulang seluruh build jika, misalnya, jumlah pengujian yang diminta berubah, meskipun itu hanya memengaruhi target pengujian (kami memiliki rencana untuk "memangkas" konfigurasi sehingga belum tentu seperti itu, tetapi belum siap).

Saat implementasi aturan memerlukan bagian dari konfigurasi, aturan harus mendeklarasikannya dalam definisinya menggunakan RuleClass.Builder.requiresConfigurationFragments(). Hal ini dilakukan untuk menghindari kesalahan (seperti aturan Python yang menggunakan fragmen Java) dan untuk memfasilitasi pemangkasan konfigurasi sehingga seperti jika opsi Python berubah, target C++ tidak perlu dianalisis ulang.

Konfigurasi aturan tidak selalu sama dengan konfigurasi aturan "induknya". Proses mengubah konfigurasi di tepi dependensi disebut "transisi konfigurasi". Ini dapat terjadi di dua tempat:

  1. Di tepi dependensi. Transisi ini ditentukan dalam Attribute.Builder.cfg() dan merupakan fungsi dari Rule (tempat transisi terjadi) dan BuildOptions (konfigurasi asli) ke satu atau beberapa BuildOptions (konfigurasi output).
  2. Pada setiap edge yang masuk ke target yang dikonfigurasi. Hal ini ditentukan dalam RuleClass.Builder.cfg().

Class yang relevan adalah TransitionFactory dan ConfigurationTransition.

Transisi konfigurasi digunakan, misalnya:

  1. Untuk mendeklarasikan bahwa dependensi tertentu digunakan selama proses build dan dependensi tersebut harus dibuat dalam arsitektur eksekusi
  2. Untuk mendeklarasikan bahwa dependensi tertentu harus dibuat untuk beberapa arsitektur (seperti untuk kode native dalam APK Android gemuk)

Jika transisi konfigurasi menghasilkan beberapa konfigurasi, hal ini disebut transisi terpisah.

Transisi konfigurasi juga dapat diterapkan di Starlark (dokumentasi di sini)

Penyedia info transitif

Penyedia info transitif adalah cara (dan _only _way) bagi target yang dikonfigurasi untuk memberi tahu berbagai hal tentang target yang dikonfigurasi lain yang bergantung padanya. Alasan pentingnya "transitif" dalam namanya adalah karena ini biasanya merupakan semacam roll-up dari penutupan transitif dari target yang dikonfigurasi.

Umumnya ada korespondensi 1:1 antara penyedia info transitif Java dan penyedia info Starlark (pengecualiannya adalah DefaultInfo yang merupakan penggabungan dari FileProvider, FilesToRunProvider, dan RunfilesProvider karena API tersebut dianggap lebih Starlark-ish daripada transliterasi langsung dari Java). Kuncinya adalah salah satu dari hal berikut ini:

  1. Objek Class Java. Ini hanya tersedia untuk penyedia yang tidak dapat diakses dari Starlark. Penyedia ini adalah subclass dari TransitiveInfoProvider.
  2. String. Hal ini sudah lama dan sangat tidak disarankan karena rentan terhadap konflik nama. Penyedia info transitif tersebut merupakan subclass langsung dari build.lib.packages.Info .
  3. Simbol penyedia. Ini dapat dibuat dari Starlark menggunakan fungsi provider() dan merupakan cara yang direkomendasikan untuk membuat penyedia baru. Simbol tersebut diwakili oleh instance Provider.Key di Java.

Penyedia baru yang diimplementasikan di Java harus diimplementasikan menggunakan BuiltinProvider. NativeProvider tidak digunakan lagi (kami belum punya waktu untuk menghapusnya) dan subclass TransitiveInfoProvider tidak dapat diakses dari Starlark.

Target yang dikonfigurasi

Target yang dikonfigurasi diimplementasikan sebagai RuleConfiguredTargetFactory. Ada subclass untuk setiap class aturan yang diterapkan di Java. Target yang dikonfigurasi Starlark dibuat melalui StarlarkRuleConfiguredTargetUtil.buildRule() .

Factory target yang dikonfigurasi harus menggunakan RuleConfiguredTargetBuilder untuk membuat nilai yang ditampilkan. Ini terdiri dari hal-hal berikut:

  1. filesToBuild, konsep kabur dari "kumpulan file yang diwakili oleh aturan ini". File ini akan dibuat saat target yang dikonfigurasi berada di command line atau dalam srcs genrule.
  2. Runfile, reguler dan data.
  3. Grup output-nya. Ini adalah berbagai "kumpulan file lain" yang dapat dibuat oleh aturan. File ini dapat diakses menggunakan atribut output_group dari aturan filegroup di BUILD dan menggunakan penyedia OutputGroupInfo di Java.

Runfile

Beberapa biner memerlukan file data untuk dijalankan. Contoh yang menonjol adalah pengujian yang membutuhkan {i>file<i} input. Hal ini diwakili dalam Bazel dengan konsep "runfiles". "Hierarki runfiles" adalah hierarki direktori file data untuk biner tertentu. Objek ini dibuat dalam sistem file sebagai hierarki symlink dengan setiap symlink yang mengarah ke file dalam hierarki sumber output.

Kumpulan runfile direpresentasikan sebagai instance Runfiles. Secara konseptual, ini adalah peta dari jalur file di hierarki runfiles ke instance Artifact yang mewakilinya. Ini sedikit lebih rumit daripada satu Map karena dua alasan:

  • Sering kali, jalur {i>runfile<i} dari sebuah file sama dengan {i>execpath<i}-nya. Kita menggunakannya untuk menghemat RAM.
  • Ada berbagai jenis entri lama dalam hierarki runfile, yang juga perlu direpresentasikan.

Runfile dikumpulkan menggunakan RunfilesProvider: instance class ini mewakili runfile target yang dikonfigurasi (seperti library) dan kebutuhan penutupan transitifnya, dan dikumpulkan seperti set bertingkat (pada kenyataannya, semuanya diimplementasikan menggunakan set bertingkat di bawah penutup): setiap target menggabungkan runfile dependensinya, menambahkan sebagian file-nya sendiri, lalu mengirimkan rangkaian yang dihasilkan ke atas dalam grafik dependensi. Instance RunfilesProvider berisi dua instance Runfiles, satu saat aturan bergantung melalui atribut "data" dan satu untuk setiap jenis dependensi yang masuk lainnya. Hal ini karena target terkadang menyajikan runfile yang berbeda saat diandalkan melalui atribut data dari yang lain. Ini adalah perilaku lama yang tidak diinginkan dan belum bisa kita hapus.

Runfile biner direpresentasikan sebagai instance RunfilesSupport. Ini berbeda dengan Runfiles karena RunfilesSupport memiliki kemampuan untuk benar-benar di-build (tidak seperti Runfiles, yang hanya merupakan pemetaan). Tindakan ini memerlukan komponen tambahan berikut:

  • Manifes runfile input. Ini adalah deskripsi serial dari hierarki runfiles. Fungsi ini digunakan sebagai proxy untuk konten hierarki runfiles dan Bazel berasumsi bahwa hierarki runfiles akan berubah jika dan hanya jika konten manifes berubah.
  • Manifes runfile output. Hal ini digunakan oleh library runtime yang menangani hierarki runfile, terutama di Windows, yang terkadang tidak mendukung link simbolis.
  • Perantara runfile. Agar ada hierarki runfiles, seseorang harus mem-build hierarki symlink dan artefak yang dituju symlink. Untuk mengurangi jumlah tepi dependensi, middleman runfile dapat digunakan untuk mewakili semua ini.
  • Argumen command line untuk menjalankan biner yang runfilenya diwakili oleh objek RunfilesSupport.

Aspek

Aspek adalah cara untuk "menyebarkan komputasi ke bawah grafik dependensi". Notifikasi tersebut dijelaskan untuk pengguna Bazel di sini. Contoh yang baik adalah buffering protokol: aturan proto_library seharusnya tidak mengetahui bahasa tertentu, tetapi membangun penerapan pesan buffering protokol ("unit dasar" buffering protokol) dalam bahasa pemrograman apa pun harus digabungkan dengan aturan proto_library sehingga jika dua target dalam bahasa yang sama bergantung pada buffering protokol yang sama, keduanya hanya akan di-build sekali.

Sama seperti target yang dikonfigurasi, target direpresentasikan di Skyframe sebagai SkyValue dan cara pembuatannya sangat mirip dengan cara target yang dikonfigurasi dibuat: target tersebut memiliki class factory bernama ConfiguredAspectFactory yang memiliki akses ke RuleContext. Namun, tidak seperti factory target yang dikonfigurasi, target ini juga mengetahui target yang dikonfigurasi dan penyedianya.

Kumpulan aspek yang disebarkan ke bawah grafik dependensi ditentukan untuk setiap atribut menggunakan fungsi Attribute.Builder.aspects(). Ada beberapa class bernama membingungkan yang berpartisipasi dalam proses ini:

  1. AspectClass adalah implementasi aspek. Ini dapat berupa di Java (dalam hal ini adalah subclass) atau di Starlark (dalam hal ini adalah instance StarlarkAspectClass). Analog dengan RuleConfiguredTargetFactory.
  2. AspectDefinition adalah definisi aspek; ini mencakup penyedia yang diperlukannya, penyedia yang disediakannya, dan berisi referensi ke implementasinya, seperti instance AspectClass yang sesuai. Fungsi ini analog dengan RuleClass.
  3. AspectParameters adalah cara untuk membuat parameter pada aspek yang disebarkan ke bawah grafik dependensi. Saat ini, string ke peta string. Contoh bagus kegunaannya adalah buffering protokol: jika suatu bahasa memiliki beberapa API, informasi tentang API mana yang menjadi tujuan pembuatan buffering protokol harus disebarkan ke bawah grafik dependensi.
  4. Aspect mewakili semua data yang diperlukan untuk menghitung aspek yang menyebarkan grafik dependensi ke bawah. Class ini terdiri dari class aspek, definisi, dan parameternya.
  5. RuleAspect adalah fungsi yang menentukan aspek mana yang harus disebarkan oleh aturan tertentu. Ini adalah fungsi Rule -> Aspect.

Detail yang tidak terduga adalah aspek dapat dilampirkan ke aspek lain; misalnya, aspek yang mengumpulkan classpath untuk Java IDE mungkin ingin mengetahui semua file .jar di classpath, tetapi beberapa di antaranya adalah buffering protokol. Dalam hal ini, aspek IDE sebaiknya dilampirkan ke pasangan aturan proto_library + aspek proto Java).

Kompleksitas aspek pada aspek ditangkap dalam class AspectCollection.

Platform dan toolchain

Bazel mendukung build multi-platform, yaitu build yang kemungkinan memiliki beberapa arsitektur yang menjalankan tindakan build dan beberapa arsitektur yang menggunakan kode tersebut. Arsitektur ini disebut sebagai platform dalam bahasa Bazel (dokumentasi lengkap di sini)

Platform dijelaskan oleh pemetaan nilai kunci dari setelan batasan (seperti konsep "arsitektur CPU") ke nilai batasan (seperti CPU tertentu seperti x86_64). Kami memiliki "kamus" untuk setelan dan nilai batasan yang paling umum digunakan dalam repositori @platforms.

Konsep toolchain berasal dari fakta bahwa bergantung pada platform tempat build dijalankan dan platform yang ditargetkan, seseorang mungkin perlu menggunakan compiler yang berbeda; misalnya, toolchain C++ tertentu dapat berjalan di OS tertentu dan dapat menargetkan beberapa OS lain. Bazel harus menentukan compiler C++ yang digunakan berdasarkan platform target dan eksekusi yang ditetapkan (dokumentasi untuk toolchain di sini).

Agar dapat melakukannya, toolchain dianotasikan dengan serangkaian eksekusi dan batasan platform target yang didukungnya. Untuk melakukannya, definisi toolchain dibagi menjadi dua bagian:

  1. Aturan toolchain() yang menjelaskan set batasan eksekusi dan target yang didukung toolchain dan memberi tahu jenis toolchain tersebut (seperti C++ atau Java) (yang diwakili oleh aturan toolchain_type())
  2. Aturan khusus bahasa yang menjelaskan toolchain yang sebenarnya (seperti cc_toolchain())

Hal ini dilakukan dengan cara ini karena kita perlu mengetahui batasan untuk setiap toolchain agar dapat melakukan resolusi toolchain dan aturan *_toolchain() khusus bahasa berisi lebih banyak informasi daripada itu, sehingga memerlukan lebih banyak waktu untuk dimuat.

Platform eksekusi ditentukan dengan salah satu cara berikut:

  1. Dalam file MODULE.bazel menggunakan fungsi register_execution_platforms()
  2. Pada command line menggunakan opsi command line --extra_execution_platforms

Kumpulan platform eksekusi yang tersedia dikomputasi dalam RegisteredExecutionPlatformsFunction .

Platform target untuk target yang dikonfigurasi ditentukan oleh PlatformOptions.computeTargetPlatform() . Ini adalah daftar platform karena pada akhirnya kami ingin mendukung beberapa platform target, tetapi belum diterapkan.

Kumpulan toolchain yang akan digunakan untuk target yang dikonfigurasi ditentukan oleh ToolchainResolutionFunction. Fungsi dari:

  • Kumpulan toolchain terdaftar (dalam file MODULE.bazel dan konfigurasi)
  • Platform eksekusi dan target yang diinginkan (dalam konfigurasi)
  • Kumpulan jenis toolchain yang diperlukan oleh target yang dikonfigurasi (dalam UnloadedToolchainContextKey)
  • Kumpulan batasan platform eksekusi target yang dikonfigurasi (atribut exec_compatible_with) dan konfigurasi (--experimental_add_exec_constraints_to_targets), dalam UnloadedToolchainContextKey

Hasilnya adalah UnloadedToolchainContext, yang pada dasarnya adalah peta dari jenis toolchain (ditunjukkan sebagai instance ToolchainTypeInfo) ke label toolchain yang dipilih. Ini disebut "unload" karena tidak berisi toolchain itu sendiri, hanya labelnya.

Kemudian, toolchain sebenarnya dimuat menggunakan ResolvedToolchainContext.load() dan digunakan oleh implementasi target terkonfigurasi yang memintanya.

Kami juga memiliki sistem lama yang mengandalkan satu konfigurasi "host" dan konfigurasi target tunggal yang diwakili oleh berbagai flag konfigurasi, seperti --cpu . Kami melakukan transisi secara bertahap ke sistem di atas. Untuk menangani kasus saat pengguna mengandalkan nilai konfigurasi lama, kami telah mengimplementasikan pemetaan platform untuk menerjemahkan antara tanda lama dan batasan platform gaya baru. Kodenya menggunakan PlatformMappingFunction dan menggunakan "little language" non-Starlark.

Batasan

Terkadang, seseorang ingin menetapkan target sebagai kompatibel dengan beberapa platform saja. Bazel memiliki (sayangnya) beberapa mekanisme untuk mencapai tujuan ini:

  • Batasan khusus aturan
  • environment_group()/environment()
  • Batasan platform

Batasan khusus aturan sebagian besar digunakan dalam aturan Google untuk Java; batasan tersebut sedang dalam proses dan tidak tersedia di Bazel, tetapi kode sumbernya mungkin berisi referensi ke aturan tersebut. Atribut yang mengatur hal ini disebut constraints= .

lingkungan_group() dan lingkungan()

Aturan ini adalah mekanisme lama dan tidak digunakan secara luas.

Semua aturan build dapat mendeklarasikan untuk "lingkungan" mana aturan tersebut dibuat, dengan "lingkungan" merupakan instance dari aturan environment().

Ada berbagai cara menentukan lingkungan yang didukung untuk sebuah aturan:

  1. Melalui atribut restricted_to=. Ini adalah bentuk spesifikasi yang paling langsung; kode ini mendeklarasikan rangkaian persis lingkungan yang didukung aturan untuk grup ini.
  2. Melalui atribut compatible_with=. Baris ini mendeklarasikan lingkungan yang didukung aturan selain lingkungan "standar" yang didukung secara default.
  3. Melalui atribut tingkat paket default_restricted_to= dan default_compatible_with=.
  4. Melalui spesifikasi default dalam environment_group() aturan. Setiap lingkungan adalah milik grup pembanding tema yang terkait (seperti "arsitektur CPU", "versi JDK", atau "sistem operasi seluler"). Definisi grup lingkungan mencakup lingkungan mana yang harus didukung oleh "default" jika tidak ditentukan oleh atribut restricted_to= / environment(). Aturan tanpa atribut seperti itu akan mewarisi semua nilai default.
  5. Melalui default class aturan. Tindakan ini akan menggantikan default global untuk semua instance dari class aturan yang ditentukan. Ini dapat digunakan, misalnya, untuk membuat semua aturan *_test dapat diuji tanpa setiap instance harus mendeklarasikan kemampuan ini secara eksplisit.

environment() diimplementasikan sebagai aturan reguler, sedangkan environment_group() merupakan subclass dari Target, tetapi bukan Rule (EnvironmentGroup) dan fungsi yang tersedia secara default dari Starlark (StarlarkLibrary.environmentGroup()) yang pada akhirnya membuat target eponim. Hal ini untuk menghindari dependensi siklik yang akan muncul karena setiap lingkungan harus mendeklarasikan grup lingkungan tempatnya berada dan setiap grup lingkungan harus mendeklarasikan lingkungan defaultnya.

Build dapat dibatasi untuk lingkungan tertentu dengan opsi command line --target_environment.

Penerapan pemeriksaan batasan dilakukan di RuleContextConstraintSemantics dan TopLevelConstraintSemantics.

Batasan platform

Cara "resmi" saat ini untuk menjelaskan platform yang kompatibel dengan target adalah dengan menggunakan batasan yang sama seperti yang digunakan untuk mendeskripsikan toolchain dan platform. Sedang dalam peninjauan dalam permintaan pull #10945.

Visibilitas

Jika Anda mengerjakan codebase besar dengan banyak developer (seperti di Google), Anda ingin berhati-hati untuk mencegah orang lain secara arbitrer bergantung pada kode Anda. Jika tidak, sesuai hukum Hyrum, orang akan akan mengandalkan perilaku yang Anda anggap sebagai detail penerapan.

Bazel mendukung hal ini dengan mekanisme yang disebut visibilitas: Anda dapat mendeklarasikan bahwa target tertentu hanya dapat diandalkan untuk menggunakan atribut visibilitas. Atribut ini sedikit spesial karena, meskipun memiliki daftar label, label tersebut dapat mengenkode pola pada nama paket, bukan pointer ke target tertentu. (Ya, ini adalah cacat desain.)

Hal ini diterapkan di tempat berikut:

  • Antarmuka RuleVisibility mewakili deklarasi visibilitas. Parameter ini dapat berupa konstanta (sepenuhnya publik atau sepenuhnya pribadi) atau daftar label.
  • Label dapat merujuk pada grup paket (daftar paket yang telah ditentukan sebelumnya), paket secara langsung (//pkg:__pkg__) atau subhierarki paket (//pkg:__subpackages__). Ini berbeda dengan sintaksis command line, yang menggunakan //pkg:* atau //pkg/....
  • Grup paket diimplementasikan sebagai targetnya sendiri (PackageGroup) dan target yang dikonfigurasi (PackageGroupConfiguredTarget). Kita mungkin dapat menggantinya dengan aturan sederhana jika ingin. Logikanya diimplementasikan dengan bantuan: PackageSpecification, yang sesuai dengan satu pola seperti //pkg/...; PackageGroupContents, yang sesuai dengan atribut packages package_group tunggal; dan PackageSpecificationProvider, yang digabungkan melalui package_group serta includes transitifnya.
  • Konversi dari daftar label visibilitas ke dependensi dilakukan di DependencyResolver.visitTargetVisibility dan beberapa tempat lainnya.
  • Pemeriksaan sebenarnya dilakukan di CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()

Kumpulan bertingkat

Sering kali, target yang dikonfigurasi menggabungkan kumpulan file dari dependensinya, menambahkannya sendiri, dan menggabungkan kumpulan tersebut ke dalam penyedia info transitif sehingga target yang dikonfigurasi dan bergantung padanya dapat melakukan hal yang sama. Contoh:

  • File header C++ yang digunakan untuk build
  • File objek yang mewakili penutupan transitif cc_library
  • Kumpulan file .jar yang perlu ada di classpath agar aturan Java dapat dikompilasi atau dijalankan
  • Kumpulan file Python dalam penutupan transitif aturan Python

Jika kita melakukan ini dengan cara naif menggunakan, misalnya, List atau Set, kita akan berakhir dengan penggunaan memori kuadrat: jika ada rantai aturan N dan setiap aturan menambahkan file, kita akan memiliki 1+2+...+N anggota koleksi.

Untuk mengatasi masalah ini, kami membuat konsep NestedSet. Ini adalah struktur data yang terdiri dari instance NestedSet lain dan beberapa anggotanya sendiri, sehingga membentuk grafik set asiklik terarah. Parameter ini tidak dapat diubah dan anggotanya dapat diiterasi. Kami menentukan beberapa urutan iterasi (NestedSet.Order): preorder, postorder, topologi (node selalu muncul setelah ancestornya) dan "don't Care, tetapi harus sama setiap saat".

Struktur data yang sama disebut depset di Starlark.

Artefak dan Tindakan

Build sebenarnya terdiri dari serangkaian perintah yang perlu dijalankan untuk menghasilkan output yang diinginkan pengguna. Perintah direpresentasikan sebagai instance class Action dan file direpresentasikan sebagai instance class Artifact. Grafik ini disusun dalam grafik asiklik bipartit yang terarah yang disebut "grafik aksi".

Artefak terdiri dari dua jenis: artefak sumber (artefak yang tersedia sebelum Bazel mulai mengeksekusi) dan artefak turunan (artefak yang perlu dibuat). Artefak turunan dapat berupa beberapa jenis:

  1. **Artefak biasa. **File ini diperiksa ke versi terbarunya dengan menghitung checksum-nya, dengan mtime sebagai pintasan; kami tidak melakukan checksum file jika waktunya belum berubah.
  2. Artefak symlink yang belum terselesaikan. Artefak ini diperiksa untuk mengetahui apakah keberadaannya merupakan yang terbaru dengan memanggil readlink(). Tidak seperti artefak biasa, artefak ini dapat berupa symlink yang menggantung. Biasanya digunakan dalam kasus ketika seseorang kemudian mengemas beberapa file ke dalam semacam arsip.
  3. Artefak pohon. Ini bukan file tunggal, tetapi hierarki direktori. File tersebut diperiksa terbarunya dengan memeriksa kumpulan file di dalamnya dan kontennya. Class tersebut direpresentasikan sebagai TreeArtifact.
  4. Artefak metadata konstan. Perubahan pada artefak ini tidak akan memicu build ulang. Ini digunakan secara eksklusif untuk informasi stempel build: kita tidak ingin melakukan build ulang hanya karena waktu saat ini berubah.

Tidak ada alasan mendasar mengapa artefak sumber tidak dapat menjadi artefak hierarki atau artefak symlink yang belum terselesaikan, hanya saja kami belum menerapkannya (seharusnya -- mereferensikan direktori sumber dalam file BUILD adalah salah satu dari beberapa masalah kesalahan yang sudah lama ada pada Bazel; kami memiliki implementasi jenis pekerjaan yang diaktifkan oleh properti JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1)

Jenis Artifact yang terkenal adalah perantara. Hal ini ditunjukkan oleh instance Artifact yang merupakan output dari MiddlemanAction. Argumen tersebut digunakan untuk beberapa hal khusus:

  • Menggabungkan perantara digunakan untuk mengelompokkan artefak. Ini bertujuan agar jika banyak tindakan menggunakan kumpulan besar input yang sama, kita tidak memiliki tepi dependensi N*M, hanya N+M (diganti dengan set bertingkat)
  • Menjadwalkan perantara dependensi memastikan bahwa tindakan berjalan sebelum tindakan yang lain. Library ini sebagian besar digunakan untuk analisis lint, tetapi juga untuk kompilasi C++ (lihat CcCompilationContext.createMiddleman() untuk penjelasannya)
  • Perantara runfile digunakan untuk memastikan keberadaan hierarki runfile sehingga tidak perlu secara terpisah bergantung pada manifes output dan setiap artefak tunggal yang dirujuk oleh hierarki runfile.

Tindakan paling baik dipahami sebagai perintah yang perlu dijalankan, lingkungan yang dibutuhkan, dan kumpulan output yang dihasilkannya. Berikut adalah komponen utama dari deskripsi tindakan:

  • Command line yang perlu dijalankan
  • Artefak input yang diperlukan
  • Variabel lingkungan yang perlu ditetapkan
  • Anotasi yang menjelaskan lingkungan (seperti platform) yang perlu dijalankan di \

Ada juga beberapa kasus khusus lainnya, seperti menulis file yang kontennya dikenal oleh Bazel. Class tersebut adalah subclass dari AbstractAction. Sebagian besar tindakannya adalah SpawnAction atau StarlarkAction (sama, keduanya tidak boleh merupakan class terpisah), meskipun Java dan C++ memiliki jenis tindakannya sendiri (JavaCompileAction, CppCompileAction, dan CppLinkAction).

Kami akhirnya ingin memindahkan semuanya ke SpawnAction; JavaCompileAction hampir sama, tetapi C++ adalah kasus khusus karena penguraian file .d dan menyertakan pemindaian.

Grafik tindakan sebagian besar "disematkan" ke dalam grafik Skyframe: secara konseptual, eksekusi tindakan direpresentasikan sebagai pemanggilan ActionExecutionFunction. Pemetaan dari tepi dependensi grafik tindakan ke tepi dependensi Skyframe dijelaskan dalam ActionExecutionFunction.getInputDeps() dan Artifact.key() serta memiliki beberapa pengoptimalan untuk menjaga agar jumlah tepi Skyframe tetap rendah:

  • Artefak turunan tidak memiliki SkyValue-nya sendiri. Sebaliknya, Artifact.getGeneratingActionKey() digunakan untuk mencari tahu kunci untuk tindakan yang menghasilkannya
  • Kumpulan bertingkat memiliki kunci Skyframe-nya sendiri.

Tindakan bersama

Beberapa tindakan dihasilkan oleh beberapa target yang dikonfigurasi; aturan Starlark lebih terbatas karena aturan hanya diizinkan untuk menempatkan tindakan turunan ke dalam direktori yang ditentukan oleh konfigurasinya dan paketnya (tetapi meskipun demikian, aturan dalam paket yang sama dapat bertentangan), tetapi aturan yang diterapkan di Java dapat menempatkan artefak turunan di mana saja.

Hal ini dianggap sebagai fitur yang salah, tetapi menghapusnya sangat sulit karena menghasilkan penghematan waktu eksekusi yang signifikan ketika, misalnya, file sumber perlu diproses dan file tersebut direferensikan oleh beberapa aturan (handwave-handwave). Ini akan berdampak pada RAM: setiap instance tindakan bersama harus disimpan dalam memori secara terpisah.

Jika dua tindakan menghasilkan file output yang sama, file tersebut harus sama persis: memiliki input, output yang sama, dan menjalankan command line yang sama. Hubungan kesetaraan ini diterapkan di Actions.canBeShared() dan diverifikasi antara fase analisis dan eksekusi dengan melihat setiap Action. Hal ini diimplementasikan di SkyframeActionExecutor.findAndStoreArtifactConflicts() dan merupakan salah satu dari beberapa tempat di Bazel yang memerlukan tampilan "global" build.

Fase eksekusi

Pada saat ini Bazel benar-benar mulai menjalankan tindakan build, seperti perintah yang menghasilkan output.

Hal pertama yang dilakukan Bazel setelah fase analisis adalah menentukan Artefak yang perlu dibuat. Logika untuk hal ini dienkode dalam TopLevelArtifactHelper; secara kasar, ini adalah filesToBuild target yang dikonfigurasi pada command line dan konten grup output khusus untuk tujuan eksplisit untuk mengekspresikan "jika target ini berada di command line, build ini artefak".

Langkah selanjutnya adalah membuat root eksekusi. Karena Bazel memiliki opsi untuk membaca paket sumber dari berbagai lokasi dalam sistem file (--package_path), Bazel perlu menyediakan tindakan yang dijalankan secara lokal dengan hierarki sumber lengkap. Hal ini ditangani oleh class SymlinkForest dan berfungsi dengan mencatat setiap target yang digunakan dalam fase analisis dan membuat satu hierarki direktori yang melakukan symlink pada setiap paket dengan target yang digunakan dari lokasi sebenarnya. Alternatifnya adalah meneruskan jalur yang benar ke perintah (dengan mempertimbangkan --package_path). Hal ini tidak diinginkan karena:

  • Ini mengubah command line tindakan saat paket dipindahkan dari entri jalur paket ke yang lain (sebelumnya kejadian umum)
  • Cara ini menghasilkan command line yang berbeda jika suatu tindakan dijalankan dari jarak jauh dan dijalankan secara lokal
  • Ini memerlukan transformasi command line khusus untuk alat yang digunakan (pertimbangkan perbedaan antara seperti classpath Java dan jalur penyertaan C++)
  • Mengubah command line suatu tindakan akan membuat entri cache tindakannya menjadi tidak valid
  • --package_path secara perlahan tidak digunakan lagi

Kemudian, Bazel mulai menjelajahi grafik tindakan (grafik terarah bipartit yang terdiri dari tindakan serta artefak input dan outputnya) dan tindakan yang berjalan. Eksekusi setiap tindakan direpresentasikan oleh instance class SkyValue ActionExecutionValue.

Karena menjalankan tindakan itu mahal, kami memiliki beberapa lapisan caching yang dapat dialami di belakang Skyframe:

  • ActionExecutionFunction.stateMap berisi data untuk membuat Skyframe dimulai ulang dari ActionExecutionFunction dengan harga murah
  • Cache tindakan lokal berisi data tentang status sistem file.
  • Sistem eksekusi jarak jauh biasanya juga memiliki {i>cache<i} sendiri

Cache tindakan lokal

Cache ini adalah lapisan lain yang berada di belakang Skyframe; meskipun suatu tindakan dijalankan kembali di Skyframe, tindakan tersebut masih dapat menjadi hit di cache tindakan lokal. Ini mewakili status sistem file lokal dan diserialisasi ke disk yang berarti bahwa saat kita memulai server Bazel baru, kita bisa mendapatkan hit cache tindakan lokal meskipun grafik Skyframe kosong.

Cache ini diperiksa untuk menemukan hit menggunakan metode ActionCacheChecker.getTokenIfNeedToExecute() .

Berbeda dengan namanya, ini adalah peta dari jalur artefak turunan ke tindakan yang memunculkannya. Tindakan tersebut dijelaskan sebagai:

  1. Kumpulan file input dan {i>output<i} dan {i>checksum<i}-nya
  2. "Tombol tindakan", yang biasanya merupakan command line yang dieksekusi, tetapi secara umum, mewakili segala sesuatu yang tidak ditangkap oleh checksum file input (misalnya untuk FileWriteAction, ini adalah checksum data yang ditulis)

Ada juga "cache tindakan top-down" yang sangat eksperimental yang masih dalam pengembangan, yang menggunakan hash transitif untuk menghindari cache sesering mungkin.

Penemuan input dan pemangkasan input

Beberapa tindakan lebih rumit daripada sekadar memiliki serangkaian input. Perubahan pada kumpulan input suatu tindakan tersedia dalam dua bentuk:

  • Suatu tindakan dapat menemukan input baru sebelum dieksekusi atau memutuskan bahwa beberapa inputnya sebenarnya tidak diperlukan. Contoh kanonisnya adalah C++, yang lebih baik untuk membuat perkiraan cerdas tentang file header apa yang digunakan file C++ dari penutupan transitifnya sehingga kita tidak perlu mengirim setiap file ke eksekutor jarak jauh; oleh karena itu, kita memiliki opsi untuk tidak mendaftarkan setiap file header sebagai "input", tetapi memindai file sumber untuk header yang saat ini disertakan secara transitif dan hanya menandai file header tersebut sebagai input yang saat ini kita sertakan dalam C++#include
  • Sebuah tindakan mungkin menyadari bahwa beberapa file tidak digunakan selama eksekusinya. Pada C++, ini disebut "file .d": compiler memberi tahu file header mana yang digunakan setelah fakta, dan untuk menghindari rasa malu karena memiliki inkrementalitas yang lebih buruk daripada Make, Bazel memanfaatkan fakta ini. Pemindai ini menawarkan perkiraan yang lebih baik daripada pemindai penyertaan karena bergantung pada compiler.

Hal ini diimplementasikan menggunakan metode di Action:

  1. Action.discoverInputs() dipanggil. Tindakan ini akan menampilkan kumpulan Artefak bertingkat yang dianggap diperlukan. Artefak tersebut harus berupa artefak sumber sehingga tidak ada edge dependensi dalam grafik tindakan yang tidak memiliki setara dalam grafik target yang dikonfigurasi.
  2. Tindakan ini dijalankan dengan memanggil Action.execute().
  3. Di akhir Action.execute(), tindakan dapat memanggil Action.updateInputs() untuk memberi tahu Bazel bahwa tidak semua inputnya diperlukan. Hal ini dapat menyebabkan build inkremental yang salah jika input yang digunakan dilaporkan sebagai tidak digunakan.

Saat cache tindakan menampilkan hit pada instance Action baru (seperti yang dibuat setelah server dimulai ulang), Bazel akan memanggil updateInputs() sendiri sehingga kumpulan input tersebut mencerminkan hasil penemuan input dan pemangkasan yang dilakukan sebelumnya.

Tindakan Starlark dapat memanfaatkan fasilitas untuk mendeklarasikan beberapa input sebagai tidak digunakan menggunakan argumen unused_inputs_list= dari ctx.actions.run().

Berbagai cara untuk menjalankan tindakan: Strategi/ActionContexts

Beberapa tindakan dapat dijalankan dengan cara yang berbeda. Misalnya, command line dapat dieksekusi secara lokal, lokal, tetapi dalam berbagai jenis sandbox, atau dari jarak jauh. Konsep yang mewujudkan hal ini disebut ActionContext (atau Strategy, karena kita hanya berhasil mengganti nama setengahnya...)

Siklus hidup konteks tindakan adalah sebagai berikut:

  1. Saat fase eksekusi dimulai, instance BlazeModule ditanya mengenai konteks tindakan apa yang dimilikinya. Hal ini terjadi di konstruktor ExecutionTool. Jenis konteks tindakan diidentifikasi oleh instance Class Java yang mengacu pada sub-antarmuka ActionContext dan antarmuka mana yang harus diterapkan oleh konteks tindakan.
  2. Konteks tindakan yang sesuai dipilih dari yang tersedia dan diteruskan ke ActionExecutionContext dan BlazeExecutor .
  3. Konteks permintaan tindakan menggunakan ActionExecutionContext.getContext() dan BlazeExecutor.getStrategy() (seharusnya hanya ada satu cara untuk melakukannya...)

Strategi dapat menggunakan strategi lain untuk melakukan tugasnya; strategi ini digunakan, misalnya, dalam strategi dinamis yang memulai tindakan secara lokal maupun jarak jauh, lalu menggunakan strategi mana yang selesai lebih dulu.

Salah satu strategi penting adalah yang mengimplementasikan proses pekerja persisten (WorkerSpawnStrategy). Idenya adalah beberapa alat memiliki waktu startup yang lama sehingga harus digunakan kembali di antara tindakan, bukan memulai yang baru untuk setiap tindakan (Hal ini mewakili potensi masalah ketepatan, karena Bazel mengandalkan janji proses pekerja bahwa alat tersebut tidak memiliki status yang dapat diamati di antara permintaan individu)

Jika alat berubah, proses pekerja perlu dimulai ulang. Apakah pekerja dapat digunakan kembali ditentukan dengan menghitung checksum untuk alat yang digunakan menggunakan WorkerFilesHash. Hal ini bergantung pada mengetahui input tindakan mana yang mewakili bagian alat dan yang mewakili input; hal ini ditentukan oleh pembuat Action: Spawn.getToolFiles() dan runfile Spawn dihitung sebagai bagian dari alat tersebut.

Informasi selengkapnya tentang strategi (atau konteks tindakan!):

  • Informasi tentang berbagai strategi untuk menjalankan tindakan tersedia di sini.
  • Informasi tentang strategi dinamis, yakni saat kami menjalankan tindakan secara lokal dan jarak jauh untuk melihat penyelesaian mana yang lebih dulu tersedia di sini.
  • Informasi tentang seluk-beluk eksekusi tindakan secara lokal tersedia di sini.

Resource manager lokal

Bazel dapat menjalankan banyak tindakan secara paralel. Jumlah tindakan lokal yang harus dijalankan secara paralel berbeda untuk setiap tindakan: makin banyak resource yang dibutuhkan, makin sedikit instance yang harus dijalankan secara bersamaan untuk menghindari beban komputer lokal secara berlebihan.

Ini diterapkan di class ResourceManager: setiap tindakan harus dianotasi dengan perkiraan resource lokal yang diperlukan dalam bentuk instance ResourceSet (CPU dan RAM). Kemudian, saat konteks tindakan melakukan sesuatu yang memerlukan resource lokal, konteks tersebut akan memanggil ResourceManager.acquireResources() dan diblokir sampai resource yang diperlukan tersedia.

Deskripsi yang lebih mendetail tentang pengelolaan resource lokal tersedia di sini.

Struktur direktori output

Setiap tindakan memerlukan tempat terpisah di direktori output tempat output-nya ditempatkan. Lokasi artefak turunan biasanya sebagai berikut:

$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>

Bagaimana nama direktori yang terkait dengan konfigurasi tertentu ditentukan? Ada dua properti yang diinginkan yang bertentangan:

  1. Jika dua konfigurasi dapat terjadi dalam build yang sama, keduanya harus memiliki direktori yang berbeda sehingga keduanya dapat memiliki versinya sendiri untuk tindakan yang sama; jika tidak, jika kedua konfigurasi tidak sepakat, misalnya command line suatu tindakan yang menghasilkan file output yang sama, Bazel tidak akan mengetahui tindakan mana yang harus dipilih ("konflik tindakan")
  2. Jika dua konfigurasi merepresentasikan "kurang lebih" sama, keduanya harus memiliki nama yang sama sehingga tindakan yang dieksekusi di salah satunya dapat digunakan kembali untuk konfigurasi lainnya jika command line cocok: misalnya, perubahan pada opsi command line ke compiler Java tidak boleh mengakibatkan tindakan kompilasi C++ dijalankan ulang.

Sejauh ini, kita belum menemukan cara berprinsip untuk menyelesaikan masalah ini, yang memiliki kesamaan dengan masalah pemangkasan konfigurasi. Pembahasan yang lebih panjang tentang opsi tersedia di sini. Area bermasalah utama adalah aturan Starlark (yang penulisnya biasanya belum terbiasa dengan Bazel) dan aspek, yang menambahkan dimensi lain ke ruang hal-hal yang dapat menghasilkan file output yang "sama".

Pendekatan saat ini adalah segmen jalur untuk konfigurasi adalah <CPU>-<compilation mode> dengan berbagai akhiran yang ditambahkan sehingga transisi konfigurasi yang diterapkan di Java tidak mengakibatkan konflik tindakan. Selain itu, checksum dari kumpulan transisi konfigurasi Starlark ditambahkan sehingga pengguna tidak dapat menyebabkan konflik tindakan. Ini jauh dari sempurna. Ini diterapkan di OutputDirectories.buildMnemonic() dan bergantung pada setiap fragmen konfigurasi yang menambahkan bagiannya sendiri ke nama direktori output.

Pengujian

Bazel memiliki dukungan yang kaya untuk menjalankan pengujian. API ini mendukung:

  • Menjalankan pengujian dari jarak jauh (jika backend eksekusi jarak jauh tersedia)
  • Menjalankan pengujian beberapa kali secara paralel (untuk deflaking atau mengumpulkan data waktu)
  • Pengujian sharding (membagi kasus pengujian dalam pengujian yang sama pada beberapa proses untuk mendapatkan kecepatan)
  • Menjalankan kembali pengujian yang tidak stabil
  • Mengelompokkan pengujian ke dalam rangkaian pengujian

Pengujian adalah target yang dikonfigurasi secara reguler dan memiliki TestProvider, yang menjelaskan cara menjalankan pengujian:

  • Artefak yang hasil build dalam pengujian yang sedang dijalankan. Ini adalah file "status cache" yang berisi pesan TestResultData serial
  • Berapa kali pengujian harus dijalankan
  • Jumlah shard tempat pengujian harus dibagi
  • Beberapa parameter tentang cara pengujian harus dijalankan (seperti waktu tunggu pengujian)

Menentukan pengujian yang akan dijalankan

Menentukan pengujian mana yang dijalankan adalah proses yang rumit.

Pertama, selama penguraian pola target, rangkaian pengujian diperluas secara rekursif. Perluasan diimplementasikan di TestsForTargetPatternFunction. Kerut yang cukup mengejutkan adalah jika rangkaian pengujian tidak mendeklarasikan pengujian, itu mengacu pada setiap pengujian dalam paketnya. Hal ini diimplementasikan di Package.beforeBuild() dengan menambahkan atribut implisit yang disebut $implicit_tests ke aturan paket pengujian.

Kemudian, pengujian difilter untuk mengetahui ukuran, tag, waktu tunggu, dan bahasa sesuai dengan opsi command line. Hal ini diterapkan di TestFilter dan dipanggil dari TargetPatternPhaseFunction.determineTests() selama penguraian target dan hasilnya dimasukkan ke dalam TargetPatternPhaseValue.getTestsToRunLabels(). Alasan mengapa atribut aturan yang dapat difilter tidak dapat dikonfigurasi adalah karena hal ini terjadi sebelum fase analisis sehingga konfigurasi tidak tersedia.

Pengujian ini kemudian diproses lebih lanjut di BuildView.createResult(): target yang analisisnya gagal akan difilter dan pengujiannya dibagi menjadi pengujian eksklusif dan non-eksklusif. Kode ini kemudian dimasukkan ke dalam AnalysisResult, yang merupakan cara ExecutionTool mengetahui pengujian mana yang akan dijalankan.

Untuk memberikan transparansi pada proses yang rumit ini, operator kueri tests() (yang diterapkan di TestsFunction) tersedia untuk mengetahui pengujian mana yang dijalankan saat target tertentu ditentukan di command line. Sayangnya, ini merupakan penerapan ulang, sehingga mungkin menyimpang dari yang di atas dalam beberapa cara yang samar.

Menjalankan pengujian

Cara pengujian dijalankan adalah dengan meminta artefak status cache. Hal ini kemudian menghasilkan eksekusi TestRunnerAction, yang pada akhirnya memanggil TestActionContext yang dipilih oleh opsi command line --test_strategy yang menjalankan pengujian dengan cara yang diminta.

Pengujian dijalankan sesuai dengan protokol mendetail yang menggunakan variabel lingkungan untuk memberi tahu pengujian apa yang diharapkan dari pengujian. Deskripsi mendetail tentang apa yang diharapkan Bazel dari pengujian dan pengujian yang dapat diharapkan dari Bazel tersedia di sini. Pada yang paling sederhana, kode keluar 0 berarti berhasil, yang lainnya berarti kegagalan.

Selain file status cache, setiap proses pengujian memunculkan sejumlah file lainnya. Direktori tersebut ditempatkan dalam "direktori log pengujian" yang merupakan subdirektori yang disebut testlogs dari direktori output konfigurasi target:

  • test.xml, file XML gaya JUnit yang memerinci setiap kasus pengujian dalam shard pengujian
  • test.log, output konsol dari pengujian. stdout dan stderr tidak dipisahkan.
  • test.outputs, "direktori output tidak dideklarasikan"; ini digunakan oleh pengujian yang ingin menghasilkan file selain yang dicetak ke terminal.

Ada dua hal yang dapat terjadi selama eksekusi uji yang tidak mungkin terjadi selama pembuatan target reguler: eksekusi uji eksklusif dan streaming output.

Beberapa pengujian perlu dijalankan dalam mode eksklusif, misalnya tidak secara paralel dengan pengujian lainnya. Hal ini dapat terjadi dengan menambahkan tags=["exclusive"] ke aturan pengujian atau menjalankan pengujian dengan --test_strategy=exclusive . Setiap pengujian eksklusif dijalankan oleh pemanggilan Skyframe terpisah yang meminta eksekusi pengujian setelah build "utama". Hal ini diterapkan di SkyframeExecutor.runExclusiveTest().

Tidak seperti tindakan reguler, yang output terminalnya dibuang saat tindakan selesai, pengguna dapat meminta output pengujian untuk di-streaming sehingga mereka mendapatkan informasi tentang progres pengujian yang berjalan lama. Hal ini ditentukan oleh opsi command line --test_output=streamed dan menyiratkan eksekusi uji eksklusif sehingga output dari pengujian yang berbeda tidak diselingi.

Hal ini diimplementasikan di class StreamedTestOutput yang diberi nama tepat dan berfungsi dengan melakukan polling perubahan pada file test.log pengujian yang dimaksud dan men-deploy byte baru ke terminal tempat aturan Bazel.

Hasil pengujian yang dijalankan tersedia di bus peristiwa dengan mengamati berbagai peristiwa (seperti TestAttempt, TestResult, atau TestingCompleteEvent). Pengujian tersebut dibuang ke Protokol Peristiwa Build dan dikeluarkan ke konsol oleh AggregatingTestListener.

Koleksi cakupan

Cakupan dilaporkan oleh pengujian dalam format LCOV dalam file bazel-testlogs/$PACKAGE/$TARGET/coverage.dat .

Untuk mengumpulkan cakupan, setiap eksekusi uji digabungkan dalam skrip yang disebut collect_coverage.sh .

Skrip ini menyiapkan lingkungan pengujian untuk mengaktifkan pengumpulan cakupan dan menentukan tempat file cakupan ditulis oleh runtime cakupan. Kemudian, pengujian akan dijalankan. Pengujian dapat menjalankan beberapa subproses dan terdiri dari bagian yang ditulis dalam beberapa bahasa pemrograman yang berbeda (dengan runtime koleksi cakupan terpisah). Skrip wrapper bertanggung jawab untuk mengonversi file yang dihasilkan ke format LCOV jika perlu, dan menggabungkannya menjadi satu file.

Interposisi collect_coverage.sh dilakukan oleh strategi pengujian dan memerlukan collect_coverage.sh untuk menjadi input pengujian. Hal ini dilakukan dengan atribut implisit :coverage_support yang diselesaikan menjadi nilai flag konfigurasi --coverage_support (lihat TestConfiguration.TestOptions.coverageSupport)

Beberapa bahasa melakukan instrumentasi offline, yang berarti bahwa instrumentasi cakupan ditambahkan pada waktu kompilasi (seperti C++) dan yang lainnya melakukan instrumentasi online, yang berarti instrumentasi cakupan ditambahkan pada waktu eksekusi.

Konsep inti lainnya adalah cakupan dasar pengukuran. Ini adalah cakupan library, biner, atau pengujian apakah tidak ada kode di dalamnya yang dijalankan. Masalah yang dipecahkan adalah jika Anda ingin menghitung cakupan pengujian untuk biner, maka tidak cukup hanya menggabungkan cakupan semua pengujian karena mungkin ada kode dalam biner yang tidak terkait dengan pengujian apa pun. Oleh karena itu, yang kami lakukan adalah menampilkan file cakupan untuk setiap biner yang hanya berisi file yang cakupannya kami kumpulkan tanpa garis tertutup. File cakupan dasar pengukuran untuk target berada di bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat . Kode ini juga dihasilkan untuk biner dan library selain untuk pengujian jika Anda meneruskan flag --nobuild_tests_only ke Bazel.

Cakupan dasar pengukuran saat ini rusak.

Kami melacak dua grup file untuk koleksi cakupan untuk setiap aturan: kumpulan file berinstrumen dan kumpulan file metadata instrumentasi.

Set file berinstrumen hanyalah itu, satu set file untuk diinstrumentasikan. Untuk runtime cakupan online, hal ini dapat digunakan saat runtime untuk menentukan file mana yang akan diinstrumentasi. Objek ini juga digunakan untuk mengimplementasikan cakupan dasar pengukuran.

Kumpulan file metadata instrumentasi adalah kumpulan file tambahan yang diperlukan pengujian untuk menghasilkan file LCOV yang diperlukan Bazel darinya. Dalam praktiknya, hal ini terdiri dari file khusus runtime; misalnya, gcc memunculkan file .gcno selama kompilasi. Ini akan ditambahkan ke kumpulan input tindakan pengujian jika mode cakupan diaktifkan.

Apakah cakupan dikumpulkan atau tidak disimpan di BuildConfiguration. Hal ini berguna karena merupakan cara mudah untuk mengubah tindakan pengujian dan grafik tindakan, bergantung pada bit ini, tetapi juga berarti bahwa jika bit ini dibalik, semua target perlu dianalisis ulang (beberapa bahasa, seperti C++ memerlukan opsi compiler yang berbeda untuk memunculkan kode yang dapat mengumpulkan cakupan, yang mengurangi masalah ini, karena itu, analisis ulang diperlukan).

File dukungan cakupan bergantung pada label dalam dependensi implisit sehingga dapat diganti oleh kebijakan pemanggilan, yang memungkinkannya berbeda di antara versi Bazel yang berbeda. Idealnya, perbedaan ini akan dihapus, dan kami menstandarkan salah satunya.

Kami juga menghasilkan "laporan cakupan" yang menggabungkan cakupan yang dikumpulkan untuk setiap pengujian dalam pemanggilan Bazel. Ini ditangani oleh CoverageReportActionFactory dan dipanggil dari BuildView.createResult() . Pengujian ini mendapatkan akses ke alat yang dibutuhkan dengan melihat atribut :coverage_report_generator dari pengujian pertama yang dijalankan.

Mesin kueri

Bazel memiliki sedikit bahasa yang digunakan untuk menanyakan berbagai hal tentang berbagai grafik. Jenis kueri berikut disediakan:

  • bazel query digunakan untuk menyelidiki grafik target
  • bazel cquery digunakan untuk menyelidiki grafik target yang dikonfigurasi
  • bazel aquery digunakan untuk menyelidiki grafik tindakan

Masing-masing diimplementasikan dengan membuat subclass AbstractBlazeQueryEnvironment. Fungsi kueri tambahan tambahan dapat dilakukan dengan membuat subclass QueryFunction. Untuk memungkinkan hasil kueri streaming, bukan mengumpulkannya ke suatu struktur data, query2.engine.Callback diteruskan ke QueryFunction, yang memanggilnya untuk hasil yang ingin ditampilkan.

Hasil kueri dapat ditampilkan dalam berbagai cara: label, label, dan class aturan, XML, protobuf, dan sebagainya. Hal ini diterapkan sebagai subclass OutputFormatter.

Persyaratan kecil dari beberapa format output kueri (tentunya proto) adalah bahwa Bazel perlu memunculkan _semua _informasi yang disediakan pemuatan paket sehingga seseorang dapat membedakan output dan menentukan apakah target tertentu telah berubah. Akibatnya, nilai atribut harus dapat diserialisasi, itulah sebabnya hanya ada sedikit jenis atribut tanpa atribut yang memiliki nilai Starlark yang kompleks. Solusi umumnya adalah menggunakan label, dan melampirkan informasi yang kompleks pada aturan dengan label tersebut. Hal ini bukan solusi yang terlalu memuaskan dan akan sangat baik jika persyaratan ini dicabut.

Sistem modul

Bazel dapat diperluas dengan menambahkan modul ke dalamnya. Setiap modul harus membuat subclass BlazeModule (namanya adalah relik histori Bazel saat dulu disebut Blaze) dan mendapatkan informasi tentang berbagai peristiwa selama eksekusi perintah.

Versi ini sebagian besar digunakan untuk mengimplementasikan berbagai fungsi "non-inti" yang hanya diperlukan oleh beberapa versi Bazel (seperti yang kami gunakan di Google):

  • Antarmuka untuk sistem eksekusi jarak jauh
  • Perintah baru

Kumpulan titik ekstensi yang ditawarkan BlazeModule agak berbahaya. Jangan gunakan sebagai contoh prinsip desain yang baik.

Bus acara

Cara utama BlazeModules berkomunikasi dengan Bazel lainnya adalah melalui bus peristiwa (EventBus): instance baru dibuat untuk setiap build, berbagai bagian Bazel dapat memposting peristiwa ke instance tersebut, dan modul dapat mendaftarkan pemroses untuk peristiwa yang diminati. Misalnya, hal-hal berikut direpresentasikan sebagai peristiwa:

  • Daftar target build yang akan dibuat telah ditentukan (TargetParsingCompleteEvent)
  • Konfigurasi tingkat atas telah ditentukan (BuildConfigurationEvent)
  • Target dibuat, berhasil atau tidak (TargetCompleteEvent)
  • Pengujian telah dijalankan (TestAttempt, TestSummary)

Beberapa peristiwa ini diwakili di luar Bazel dalam Build Event Protocol (yaitu BuildEvent). Hal ini tidak hanya memungkinkan BlazeModule, tetapi juga hal-hal di luar proses Bazel untuk mengamati build. Library ini dapat diakses sebagai file yang berisi pesan protokol, atau Bazel dapat terhubung ke server (disebut Layanan Peristiwa Build) untuk mengalirkan peristiwa.

Hal ini diimplementasikan dalam paket Java build.lib.buildeventservice dan build.lib.buildeventstream.

Repositori eksternal

Meskipun Bazel awalnya didesain untuk digunakan dalam monorepo (satu pohon sumber yang berisi semua yang perlu dibuat oleh developer), Bazel berada di dunia yang belum tentu benar. "Repositori eksternal" adalah abstraksi yang digunakan untuk menjembatani dua dunia ini: repositori mewakili kode yang diperlukan untuk build, tetapi tidak dalam hierarki sumber utama.

File WORKSPACE

Kumpulan repositori eksternal ditentukan dengan menguraikan file WORKSPACE. Misalnya, deklarasi seperti ini:

    local_repository(name="foo", path="/foo/bar")

Hasil di repositori bernama @foo tersedia. Jika hal ini menjadi rumit, kita dapat menentukan aturan repositori baru dalam file Starlark, yang kemudian dapat digunakan untuk memuat kode Starlark baru, yang dapat digunakan untuk menentukan aturan repositori baru dan seterusnya...

Untuk menangani kasus ini, penguraian file WORKSPACE (dalam WorkspaceFileFunction) dibagi menjadi beberapa bagian yang ditunjukkan oleh pernyataan load(). Indeks potongan ditunjukkan oleh WorkspaceFileKey.getIndex() dan menghitung WorkspaceFileFunction hingga indeks X berarti mengevaluasinya hingga pernyataan load() ke-X.

Mengambil repositori

Sebelum kode repositori tersedia untuk Bazel, kode tersebut harus fetched. Ini menyebabkan Bazel membuat direktori di bawah $OUTPUT_BASE/external/<repository name>.

Pengambilan repositori terjadi pada langkah-langkah berikut:

  1. PackageLookupFunction menyadari bahwa kode tersebut memerlukan repositori dan membuat RepositoryName sebagai SkyKey, yang memanggil RepositoryLoaderFunction
  2. RepositoryLoaderFunction meneruskan permintaan ke RepositoryDelegatorFunction karena alasan yang tidak jelas (kode mengatakan bahwa ini untuk menghindari mendownload ulang jika Skyframe dimulai ulang, tetapi ini bukan alasan yang sangat kuat)
  3. RepositoryDelegatorFunction mengetahui aturan repositori yang diminta untuk diambil dengan melakukan iterasi pada potongan file WORKSPACE hingga repositori yang diminta ditemukan
  4. Ditemukan RepositoryFunction yang sesuai untuk mengimplementasikan pengambilan repositori; ini adalah implementasi Starlark dari repositori atau peta hard code untuk repositori yang diimplementasikan di Java.

Ada berbagai lapisan cache karena pengambilan repositori bisa sangat mahal:

  1. Ada cache untuk file yang didownload yang dikunci oleh checksum (RepositoryCache). Hal ini mengharuskan checksum tersedia di file WORKSPACE, tetapi hal ini tetap bagus untuk hermetisitas. Ini digunakan bersama oleh setiap instance server Bazel di workstation yang sama, terlepas dari ruang kerja atau basis output mana yang dijalankan.
  2. "File penanda" ditulis untuk setiap repositori pada $OUTPUT_BASE/external yang berisi checksum aturan yang digunakan untuk mengambilnya. Jika server Bazel dimulai ulang tetapi checksum tidak berubah, server tidak akan diambil ulang. Hal ini diimplementasikan di RepositoryDelegatorFunction.DigestWriter .
  3. Opsi command line --distdir menetapkan cache lain yang digunakan untuk mencari artefak yang akan didownload. Hal ini berguna dalam setelan perusahaan saat Bazel tidak boleh mengambil hal acak dari Internet. Hal ini diimplementasikan oleh DownloadManager .

Setelah repositori didownload, artefak di dalamnya diperlakukan sebagai artefak sumber. Hal ini menimbulkan masalah karena Bazel biasanya memeriksa informasi terbaru artefak sumber dengan memanggil stat() pada artefak tersebut, dan artefak ini juga dibatalkan saat definisi repositori tempatnya berubah. Oleh karena itu, FileStateValue untuk artefak di repositori eksternal harus bergantung pada repositori eksternalnya. Tindakan ini ditangani oleh ExternalFilesHelper.

Pemetaan repositori

Bisa terjadi bahwa beberapa repositori ingin bergantung pada repositori yang sama, tetapi dalam versi yang berbeda (ini adalah instance dari "masalah dependensi diamond"). Misalnya, jika dua biner dalam repositori terpisah dalam build ingin bergantung pada Guava, keduanya mungkin akan merujuk ke Guava dengan label yang dimulai @guava// dan mengharapkannya berarti versi yang berbeda.

Oleh karena itu, Bazel memungkinkan seseorang memetakan ulang label repositori eksternal sehingga string @guava// dapat merujuk ke satu repositori Guava (seperti @guava1//) di repositori satu biner dan repositori Guava lainnya (seperti @guava2//) repositori yang lainnya.

Atau, metode ini juga dapat digunakan untuk join dengan berlian. Jika repositori bergantung pada @guava1//, dan repositori lainnya bergantung pada @guava2//, pemetaan repositori memungkinkan satu repositori memetakan ulang kedua repositori untuk menggunakan repositori @guava// kanonis.

Pemetaan ditentukan dalam file WORKSPACE sebagai atribut repo_mapping dari definisi repositori individu. Elemen ini kemudian muncul di Skyframe sebagai anggota WorkspaceFileValue, tempatnya dihubungkan ke:

  • Package.Builder.repositoryMapping yang digunakan untuk mengubah atribut aturan bernilai label dalam paket dengan RuleClass.populateRuleAttributeValues()
  • Package.repositoryMapping yang digunakan dalam fase analisis (untuk menyelesaikan hal-hal seperti $(location) yang tidak diuraikan dalam fase pemuatan)
  • BzlLoadFunction untuk me-resolve label dalam pernyataan load()

Bit JNI

Server Bazel sebagian besar ditulis dalam Java. Namun, ada pengecualian untuk bagian-bagian yang tidak dapat dilakukan Java sendiri atau tidak dapat melakukannya sendiri saat kita mengimplementasikannya. Hal ini sebagian besar terbatas pada interaksi dengan sistem file, kontrol proses, dan berbagai hal tingkat rendah lainnya.

Kode C++ berada di bawah src/main/native dan class Java dengan metode native adalah:

  • NativePosixFiles dan NativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperations dan WindowsFileProcesses
  • com.google.devtools.build.lib.platform

Output konsol

Memberikan output konsol tampak seperti hal yang sederhana, tetapi pertemuan menjalankan beberapa proses (terkadang dari jarak jauh), caching yang mendetail, keinginan untuk memiliki output terminal yang bagus dan berwarna-warni, serta memiliki server yang berjalan lama membuatnya sulit.

Tepat setelah panggilan RPC masuk dari klien, dua instance RpcOutputStream akan dibuat (untuk stdout dan stderr) yang meneruskan data yang dicetak ke klien. Keduanya kemudian digabungkan dalam pasangan OutErr (pasangan (stdout, stderr). Semua yang perlu dicetak di konsol akan melalui aliran ini. Kemudian aliran data ini diteruskan ke BlazeCommandDispatcher.execExclusively().

Output secara default dicetak dengan urutan escape ANSI. Jika tidak diinginkan (--color=no), parameter akan dihapus oleh AnsiStrippingOutputStream. Selain itu, System.out dan System.err dialihkan ke aliran output ini. Hal ini bertujuan agar informasi proses debug dapat dicetak menggunakan System.err.println() dan masih berakhir di output terminal klien (yang berbeda dari server). Jika suatu proses menghasilkan output biner (seperti bazel query --output=proto), munging stdout tidak akan dilakukan.

Pesan singkat (error, peringatan, dan sejenisnya) dinyatakan melalui antarmuka EventHandler. Secara khusus, ini berbeda dari apa yang diposting oleh satu ke EventBus (ini membingungkan). Setiap Event memiliki EventKind (error, peringatan, info, dan beberapa lainnya) dan mungkin memiliki Location (tempat dalam kode sumber yang menyebabkan peristiwa terjadi).

Beberapa implementasi EventHandler menyimpan peristiwa yang diterima. Ini digunakan untuk memutar ulang informasi ke UI yang disebabkan oleh berbagai jenis pemrosesan yang di-cache, misalnya, peringatan yang dikeluarkan oleh target yang dikonfigurasi dalam cache.

Beberapa EventHandler juga memungkinkan postingan peristiwa yang pada akhirnya akan masuk ke bus peristiwa (Event reguler _tidak _muncul di sana). Ini adalah implementasi ExtendedEventHandler dan penggunaan utamanya adalah untuk memutar ulang peristiwa EventBus yang di-cache. Semua peristiwa EventBus ini mengimplementasikan Postable, tetapi tidak semua yang diposting ke EventBus selalu mengimplementasikan antarmuka ini; hanya peristiwa yang di-cache oleh ExtendedEventHandler (akan lebih baik dan sebagian besar hal tersebut melakukannya; meskipun tidak diterapkan)

Output terminal sebagian besar dipancarkan melalui UiEventHandler, yang bertanggung jawab atas semua pemformatan output yang canggih dan pelaporan progres yang dilakukan Bazel. Memiliki dua input:

  • Bus acara
  • Aliran peristiwa diteruskan melalui Reporter

Satu-satunya koneksi langsung yang dimiliki mesin eksekusi perintah (misalnya Bazel lainnya) ke streaming RPC ke klien adalah melalui Reporter.getOutErr(), yang memungkinkan akses langsung ke streaming ini. Ini hanya digunakan ketika perintah perlu membuang sejumlah besar kemungkinan data biner (seperti bazel query).

Membuat Profil Bazel

Bazel cepat. Bazel juga lambat, karena build cenderung berkembang hingga hanya mencapai batas dari yang mampu. Karena alasan ini, Bazel menyertakan profiler yang dapat digunakan untuk membuat profil build dan Bazel itu sendiri. Ini diimplementasikan di class yang tepat bernama Profiler. Fitur ini diaktifkan secara default, meskipun hanya mencatat data ringkas sehingga overhead-nya dapat ditoleransi; Command line --record_full_profiler_data membuatnya merekam semua yang bisa dilakukan.

Profil akan ditampilkan dalam format profiler Chrome; yang paling baik dilihat di Chrome. Model datanya adalah stack tugas: seseorang dapat memulai tugas dan mengakhiri tugas dan keduanya seharusnya disusun bertingkat dengan rapi satu sama lain. Setiap thread Java mendapatkan stack tugasnya sendiri. TODO: Bagaimana cara kerjanya dengan tindakan dan gaya penerusan kelanjutan?

Profiler dimulai dan dihentikan di BlazeRuntime.initProfiler() dan BlazeRuntime.afterCommand(), serta berupaya aktif selama mungkin sehingga kami dapat membuat profil semuanya. Untuk menambahkan sesuatu ke profil, panggil Profiler.instance().profile(). Metode ini menampilkan Closeable, yang penutupannya mewakili akhir tugas. Cara ini paling baik digunakan dengan pernyataan coba dengan sumber daya.

Kita juga melakukan pembuatan profil memori dasar di MemoryProfiler. Emulator tersebut juga selalu aktif dan sebagian besar merekam ukuran heap maksimum dan perilaku GC.

Menguji Bazel

Bazel memiliki dua jenis pengujian utama: pengujian yang mengamati Bazel sebagai "kotak hitam" dan pengujian yang hanya menjalankan fase analisis. Kami menyebut "pengujian integrasi" sebelumnya dan yang kedua sebagai "pengujian unit", meskipun lebih seperti pengujian integrasi yang kurang terintegrasi. Kami juga memiliki beberapa pengujian unit yang sebenarnya, yang diperlukan.

Untuk pengujian integrasi, kami memiliki dua jenis:

  1. Salah satu diimplementasikan menggunakan framework pengujian bash yang sangat rumit di bawah src/test/shell
  2. Salah satu yang diimplementasikan di Java. Hal ini diterapkan sebagai subclass BuildIntegrationTestCase

BuildIntegrationTestCase adalah framework pengujian integrasi pilihan karena dilengkapi dengan baik untuk sebagian besar skenario pengujian. Karena merupakan framework Java, framework ini menyediakan kemampuan debug dan integrasi yang lancar dengan banyak alat pengembangan umum. Ada banyak contoh class BuildIntegrationTestCase di repositori Bazel.

Pengujian analisis diimplementasikan sebagai subclass dari BuildViewTestCase. Ada sistem file awal yang dapat Anda gunakan untuk menulis file BUILD, lalu berbagai metode helper dapat meminta target yang dikonfigurasi, mengubah konfigurasi, dan menyatakan berbagai hal tentang hasil analisis.