Basis Kode Bazel

Laporkan masalah Lihat sumber {/18/}{/1/}

Dokumen ini berisi deskripsi tentang code base dan struktur Bazel. Alat ini ditujukan untuk orang yang bersedia berkontribusi pada Bazel, bukan untuk pengguna akhir.

Pengantar

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

Agar orang-orang di tengah perjalanan tidak berada di dalam hutan gelap dengan jalur yang mudah hilang, dokumen ini mencoba memberikan ringkasan tentang code base sehingga lebih mudah untuk mulai mengerjakannya.

Versi publik kode sumber Bazel tersedia di GitHub di github.com/bazelbuild/bazel. Ini bukanlah “sumber kebenaran”; sumber 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 dalam hierarki sumber internal, lalu diekspor kembali ke GitHub.

Arsitektur klien/server

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

Itulah sebabnya command line Bazel memiliki dua jenis opsi: startup dan command. Dalam command line seperti ini:

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

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

Setiap instance server memiliki satu hierarki sumber terkait ("ruang kerja") dan setiap ruang kerja biasanya memiliki satu instance server aktif. Hal ini dapat dilewati dengan menentukan basis output kustom (lihat bagian "Tata letak direktori" untuk informasi selengkapnya).

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

  1. Memeriksa apakah aplikasi telah mengekstrak dirinya sendiri. Jika tidak, sistem akan melakukannya. Dari sinilah implementasi server berasal.
  2. Memeriksa apakah ada instance server aktif yang berfungsi: instance server sedang berjalan, memiliki opsi startup yang tepat, dan menggunakan direktori ruang kerja yang tepat. Aplikasi ini menemukan server yang berjalan dengan melihat direktori $OUTPUT_BASE/server yang terdapat file kunci dengan port yang didengarkan oleh server.
  3. Jika perlu, menghentikan proses server lama
  4. Jika diperlukan, mulai proses server baru

Setelah proses server yang sesuai sudah siap, perintah yang perlu dijalankan dikomunikasikan ke proses tersebut melalui antarmuka gRPC, lalu output Bazel disalurkan kembali ke terminal. Hanya satu perintah yang dapat dijalankan secara bersamaan. Hal ini diimplementasikan menggunakan mekanisme penguncian yang rumit dengan bagian-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 cukup memalukan. Pemblokir utama adalah siklus proses BlazeModule dan beberapa status di BlazeRuntime.

Di akhir perintah, server Bazel mengirimkan kode keluar yang harus ditampilkan klien. Masalah 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 sebagai gantinya, memberitahu klien biner apa yang harus dilakukan ujexec() dan dengan argumen apa.

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

Kode sumber klien berada di bawah src/main/cpp dan protokol yang digunakan untuk berkomunikasi dengan server berada 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 serangkaian direktori yang agak rumit selama proses build. Deskripsi lengkap tersedia di Tata letak direktori output.

"workspace" adalah hierarki sumber tempat Bazel dijalankan. Hal ini biasanya berkaitan dengan sesuatu yang Anda periksa dari kontrol sumber.

Bazel meletakkan semua datanya di bawah "root pengguna {i>output<i}". Nilai 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-nya berdasarkan basis penginstalan. Semuanya berada di $OUTPUT_USER_ROOT/install secara default dan dapat diubah menggunakan opsi command line --install_base.

"Basis output" adalah tempat instance Bazel yang terpasang ke ruang kerja tertentu melakukan operasi tulis. Tiap 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 keterbatasan 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 build, direktori tugasnya adalah $EXECROOT/<name of main repository>. Kami berencana mengubahnya menjadi $EXECROOT, meskipun merupakan paket jangka panjang karena merupakan perubahan yang sangat tidak kompatibel.
  • File yang dibuat selama build.

Proses mengeksekusi perintah

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

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

  2. Perintah yang tepat ditemukan. Setiap perintah harus mengimplementasikan antarmuka BlazeCommand dan harus memiliki anotasi @Command (ini sedikit antipola. Sebaiknya jika semua metadata yang diperlukan perintah dijelaskan oleh metode di BlazeCommand)

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

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

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

  6. Kumpulan pola target pada command line diuraikan dan karakter pengganti seperti //pkg:all dan //pkg/... telah diselesaikan. Hal ini diimplementasikan di AnalysisPhaseRunner.evaluateTargetPatterns() dan diperbarui di Skyframe sebagai TargetPatternPhaseValue.

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

  8. Fase eksekusi dijalankan. Artinya, setiap tindakan yang diperlukan untuk membangun 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 yang saling terkait satu sama lain. Contoh:

  1. Opsi yang terkait dengan bahasa pemrograman (CppOptions atau JavaOptions). Opsi ini harus berupa subclass 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 perlu melakukan C++ menyertakan pemindaian atau tidak) dibaca pada fase eksekusi, tetapi proses tersebut selalu memerlukan pipeline eksplisit karena BuildConfiguration tidak tersedia pada saat itu. Untuk mengetahui informasi selengkapnya, lihat bagian “Konfigurasi”.

PERINGATAN: Kami berpura-pura bahwa instance OptionsBase tidak dapat diubah dan menggunakannya seperti itu (misalnya sebagai bagian dari SkyKeys). Hal ini tidak terjadi dan mengubahnya adalah cara yang sangat bagus untuk merusak Bazel dengan cara halus yang sulit di-debug. Sayangnya, membuatnya benar-benar tidak dapat diubah adalah upaya besar. (Anda dapat mengubah FragmentOptions segera setelah konstruksi 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 telah dihubungkan dengan Bazel (CommonCommandOptions)
  2. Dari anotasi @Command pada 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 Java nilai opsi command line biasanya berupa 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 ke jenis data akan jatuh ke implementasi com.google.devtools.common.options.Converter.

Pohon sumber, seperti yang terlihat oleh Bazel

Bazel menangani masalah membangun software, yang dilakukan dengan membaca dan menafsirkan kode sumber. Totalitas kode sumber yang dioperasikan Bazel disebut "ruang kerja" dan disusun menjadi repositori, paket, dan aturan.

Repositori

"Repositori" adalah hierarki sumber tempat developer bekerja; biasanya mewakili satu project. Leluhur Bazel, Blaze, beroperasi di monorepo, yaitu satu hierarki 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 oleh file bernama WORKSPACE (atau WORKSPACE.bazel) di direktori root-nya. File ini berisi informasi yang bersifat "global" untuk seluruh build, misalnya, kumpulan repositori eksternal yang tersedia. File ini berfungsi seperti file Starlark biasa, yang berarti file ini dapat melakukan load() file Starlark lainnya. Ini biasanya digunakan untuk menarik repositori yang diperlukan oleh repositori yang direferensikan secara eksplisit (kami menyebutnya "pola deps.bzl")

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

Saat menjalankan build, seluruh hierarki sumber perlu disatukan; ini dilakukan oleh SymlinkForest, yang menghubungkan setiap paket di repositori utama ke $EXECROOT dan setiap repositori eksternal ke $EXECROOT/external atau $EXECROOT/.. (yang pertama tentu saja tidak memungkinkan untuk memiliki paket bernama external di repositori utama; itulah sebabnya kami bermigrasi dari repositori ini)

Paket

Setiap repositori terdiri dari paket, kumpulan file terkait, dan spesifikasi dependensi. Hal ini ditentukan oleh file yang disebut BUILD atau BUILD.bazel. Jika keduanya ada, Bazel lebih memilih BUILD.bazel; alasan mengapa file BUILD masih diterima adalah karena leluhur Bazel, Blaze, menggunakan nama file ini. Namun, segmen ini ternyata menjadi segmen jalur yang biasa digunakan, terutama di Windows, yang tidak peka huruf besar/kecil.

Paket tidak saling bergantung: perubahan pada file BUILD paket tidak dapat menyebabkan paket lain berubah. Penambahan atau penghapusan file BUILD _can _change paket lainnya, karena bola rekursif berhenti pada batas paket sehingga keberadaan file BUILD menghentikan rekursi.

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

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

Globbing diimplementasikan di class berikut:

  • LegacyGlobber, globber yang tidak sadar Skyframe dan cepat
  • SkyframeHybridGlobber, versi yang menggunakan Skyframe dan kembali ke globber lama untuk menghindari “Skyframe memulai ulang” (dijelaskan di bawah)

Class Package itu sendiri berisi beberapa anggota yang digunakan secara eksklusif untuk mengurai file WORKSPACE dan yang tidak masuk akal untuk paket sebenarnya. Hal ini adalah cacat desain karena objek yang menjelaskan paket reguler tidak boleh berisi kolom yang menjelaskan hal lain. Contoh tersebut meliputi:

  • Pemetaan repositori
  • Toolchain terdaftar
  • Platform eksekusi yang terdaftar

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

Label, Target, dan Aturan

Paket terdiri dari target yang memiliki jenis berikut:

  1. File: hal-hal yang merupakan input atau output build. Dalam bahasa Bazel, kami menyebutnya artefak (dibahas di tempat lain). Tidak semua file yang dibuat selama build merupakan target; biasanya output Bazel tidak memiliki label terkait.
  2. Aturan: ini menjelaskan langkah-langkah untuk memperoleh output-nya 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 label 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 terkait dengan direktori paket. Saat merujuk ke target pada command line, beberapa bagian label dapat dihilangkan:

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

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

Class aturan Starlark perlu diimpor di awal file BUILD menggunakan pernyataan load(), sedangkan class aturan Java "secara bawaan" diketahui oleh Bazel, karena telah didaftarkan dengan ConfiguredRuleClassProvider.

Class aturan berisi informasi seperti:

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

Catatan terminologi: Dalam code base, kami sering menggunakan “Aturan” yang berarti target yang dibuat oleh class aturan. Namun dalam Starlark dan dalam dokumentasi yang ditampilkan kepada pengguna, “Rule” harus digunakan secara eksklusif untuk merujuk ke class aturan itu sendiri; target hanyalah “target”. Perlu diperhatikan juga bahwa meskipun RuleClass memiliki “class” dalam namanya, tidak ada hubungan pewarisan Java antara class aturan dan target jenis tersebut.

Rangka Langit

Kerangka kerja evaluasi yang mendasari Bazel disebut Skyframe. Modelnya adalah semua yang perlu dibangun selama build diatur menjadi grafik asiklik terarah dengan tepi yang mengarah dari setiap bagian data ke dependensinya, yaitu, bagian lain dari data yang perlu diketahui untuk menyusunnya.

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 (seperti untuk class opsi individual BuildOptions, yang merupakan anggota dari BuildConfigurationValue dan SkyKey-nya), kami berusaha sangat keras untuk tidak mengubahnya atau mengubahnya hanya dengan cara yang tidak dapat diamati dari luar. Dari sini, semua yang dikomputasi dalam Skyframe (seperti target yang dikonfigurasi) juga harus tidak dapat diubah.

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

Skyframe tersedia dalam paket com.google.devtools.build.skyframe. Paket com.google.devtools.build.lib.skyframe dengan nama yang mirip berisi implementasi Bazel di atas Skyframe. Informasi selengkapnya tentang Skyframe tersedia di sini.

Untuk mengevaluasi SkyKey tertentu menjadi SkyValue, Skyframe akan memanggil SkyFunction yang sesuai dengan jenis kunci. Selama evaluasi, fungsi dapat meminta dependensi lain dari Skyframe dengan memanggil berbagai overload SkyFunction.Environment.getValue(). Hal ini memiliki efek samping dari pendaftaran dependensi tersebut ke dalam grafik internal Skyframe, sehingga Skyframe dapat mengevaluasi ulang fungsi saat salah satu dependensinya berubah. Dengan kata lain, caching dan komputasi inkremental Skyframe bekerja dengan 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 sendirinya, sehingga menampilkan null. Pada waktu mendatang, Skyframe akan mengevaluasi dependensi yang tidak tersedia, lalu memulai ulang fungsi tersebut dari awal — hanya kali ini panggilan getValue() akan berhasil dengan hasil non-null.

Konsekuensinya adalah setiap komputasi yang dilakukan di dalam SkyFunction sebelum mulai ulang harus diulang. Namun, 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 beberapa batch (dengan menggunakan getValuesAndExceptions()) untuk membatasi jumlah mulai ulang.
  2. Memecah SkyValue menjadi bagian-bagian terpisah yang dikomputasi oleh berbagai SkyFunction, sehingga dapat dikomputasi dan di-cache secara independen. Hal ini harus dilakukan secara strategis karena berpotensi meningkatkan penggunaan memori.
  3. Menyimpan status di antara mulai ulang, baik menggunakan SkyFunction.Environment.getState(), atau menyimpan cache statis ad hoc "di belakang Skyframe".

Pada dasarnya, kita memerlukan jenis solusi ini karena secara rutin kami memiliki ratusan ribu node Skyframe yang sedang beroperasi, dan Java tidak mendukung thread ringan.

Starlark

Starlark adalah bahasa khusus domain yang digunakan orang untuk mengonfigurasi dan memperluas Bazel. Alat ini dipahami 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 memungkinkan pembacaan serentak. Ini bukanlah pengujian yang telah selesai, sehingga beberapa (tetapi tidak semua) pengguna enggan menyelesaikan tugas pemrograman umum dalam bahasa tersebut.

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

Starlark digunakan dalam beberapa konteks, antara lain:

  1. Bahasa BUILD. Di sinilah aturan baru ditentukan. Kode Starlark yang berjalan dalam konteks ini hanya memiliki akses ke konten file BUILD itu sendiri dan file .bzl yang dimuat olehnya.
  2. Definisi aturan. Ini adalah cara penentuan aturan baru (seperti dukungan untuk bahasa baru). Kode Starlark yang berjalan dalam konteks ini memiliki akses ke konfigurasi dan data yang disediakan oleh dependensi langsungnya (selengkapnya tentang ini nanti).
  3. File WORKSPACE. Di sinilah repositori eksternal (kode yang tidak ada dalam hierarki sumber utama) ditentukan.
  4. Definisi aturan repositori. Di sinilah jenis repositori eksternal baru ditentukan. Kode Starlark yang berjalan dalam konteks ini dapat menjalankan kode arbitrer pada mesin tempat Bazel berjalan, dan menjangkau ke luar ruang kerja.

Dialek yang tersedia untuk file BUILD dan .bzl sedikit berbeda karena mengekspresikan hal yang berbeda. Daftar perbedaan 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 wajar, merupakan pasangan (target, konfigurasi).

Fase ini disebut "fase pemuatan/analisis" karena dapat dibagi menjadi dua bagian berbeda, yang sebelumnya merupakan serial, tetapi sekarang bisa saling tumpang tindih dalam waktu:

  1. Memuat paket, yaitu mengubah file BUILD menjadi objek Package yang mewakilinya
  2. Menganalisis target yang dikonfigurasi, yaitu menjalankan penerapan 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; yaitu, node daun terlebih dahulu, lalu hingga node pada command line. Input untuk analisis satu target yang dikonfigurasi adalah:

  1. Konfigurasi. ("cara" membangun aturan tersebut; misalnya, platform target tetapi juga hal-hal seperti opsi command line yang ingin diteruskan pengguna ke compiler C++)
  2. Dependensi langsung. Penyedia info transitifnya tersedia untuk aturan yang sedang dianalisis. Class tersebut dipanggil seperti itu karena menyediakan "roll-up" informasi saat penutupan transitif target yang dikonfigurasi, seperti semua file .jar di classpath atau semua file .o yang perlu ditautkan ke biner C++)
  3. Targetnya sendiri. Ini adalah hasil dari pemuatan paket tempat target berada. Untuk aturan, hal ini mencakup atributnya, yang biasanya merupakan hal yang penting.
  4. Implementasi target yang dikonfigurasi. Untuk aturan, ini bisa dilakukan di Starlark atau di Java. Semua target yang dikonfigurasi tanpa aturan diimplementasikan 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 canggih, tetapi pada saat yang sama, lebih mudah untuk melakukan Bad ThingsTM, misalnya menulis kode yang kompleksitas waktu atau ruangnya kuadrat (atau lebih buruk), membuat server Bazel error dengan pengecualian Java, atau melanggar invarian (seperti secara tidak sengaja mengubah instance Options atau dengan membuat target yang dikonfigurasi dapat diubah)

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

Konfigurasi

Konfigurasi adalah "cara" mem-build target: untuk platform apa, dengan opsi command line, dll.

Target yang sama dapat dibangun untuk beberapa konfigurasi dalam build yang sama. Hal ini berguna, misalnya ketika kode yang sama digunakan untuk alat yang dijalankan selama build serta untuk kode target, dan saat kita melakukan kompilasi silang atau membangun aplikasi Android gemuk (yang berisi kode native untuk beberapa arsitektur CPU)

Secara konseptual, konfigurasi adalah instance BuildOptions. Namun, pada praktiknya, BuildOptions digabungkan oleh BuildConfiguration yang memberikan fungsi tambahan yang beragam. Dependensi ini disebarkan dari atas grafik dependensi ke bawah. Jika berubah, build harus 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 berencana untuk "memangkas" konfigurasi sehingga hal ini tidak terjadi, tetapi belum siap).

Jika penerapan aturan memerlukan bagian dari konfigurasi, penerapan 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 harus sama dengan aturan "induknya" . Proses perubahan konfigurasi pada edge dependensi disebut "transisi konfigurasi". Hal ini dapat terjadi di dua tempat:

  1. Di edge 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. Di setiap edge 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 build sehingga dependensi tersebut harus di-build dalam arsitektur eksekusi
  2. Untuk mendeklarasikan bahwa dependensi tertentu harus dibuat untuk beberapa arsitektur (misalnya untuk kode native dalam APK Android berukuran besar)

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

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

Penyedia info transitif

Penyedia info transitif adalah cara (dan _satu-satunya _way) bagi target yang dikonfigurasi untuk memberi tahu hal-hal tentang target lain yang dikonfigurasi dan bergantung padanya. Nama "transitif" digunakan pada namanya adalah karena ini biasanya merupakan semacam gabungan dari penutupan transitif dari target yang dikonfigurasi.

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

  1. Objek Kelas Java. Ini hanya tersedia untuk penyedia yang tidak dapat diakses dari Starlark. Penyedia ini adalah subclass TransitiveInfoProvider.
  2. Sebuah {i>string<i}. Hal ini merupakan warisan dan sangat tidak disarankan karena rentan terhadap pertentangan nama. Penyedia info transitif tersebut adalah 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 ini diwakili oleh instance Provider.Key di Java.

Penyedia baru yang diterapkan 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 diterapkan sebagai RuleConfiguredTargetFactory. Terdapat 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. Kode ini terdiri dari hal-hal berikut:

  1. filesToBuild-nya, konsep buram "kumpulan file yang diwakili aturan ini". Ini adalah file yang di-build saat target yang dikonfigurasi berada di command line atau di src dari genrule.
  2. {i>Runfile<i}, reguler, dan data.
  3. Grup output mereka. Ini adalah berbagai "kumpulan file lain" yang dapat dibuat oleh aturan. Class ini dapat diakses menggunakan atribut output_group dari aturan filegroup di BUILD dan menggunakan penyedia OutputGroupInfo di Java.

{i>Runfile<i}

Beberapa biner memerlukan file data agar dapat dijalankan. Contoh yang jelas adalah pengujian yang memerlukan file input. Hal ini direpresentasikan dalam Bazel oleh konsep "runfiles". "Pohon runfiles" adalah hierarki direktori file data untuk biner tertentu. Library ini dibuat di sistem file sebagai hierarki symlink dengan masing-masing symlink yang mengarah ke file dalam sumber hierarki output.

Kumpulan runfile direpresentasikan sebagai instance Runfiles. Secara konseptual, peta dari jalur file dalam 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, serta dikumpulkan seperti kumpulan bertingkat (faktanya, instance diimplementasikan menggunakan kumpulan bertingkat dalam penutup): setiap target menyatukan runfile dependensinya, menambahkan beberapa targetnya sendiri, lalu mengirimkan kumpulan yang dihasilkan ke atas dalam grafik dependensi. Instance RunfilesProvider berisi dua instance Runfiles, satu untuk saat aturan bergantung melalui atribut "data" dan satu untuk setiap jenis dependensi yang masuk lainnya. Hal ini karena target terkadang menyajikan runfile yang berbeda jika diandalkan melalui atribut data daripada sebaliknya. Ini adalah perilaku lama yang tidak diinginkan dan belum kami hapus.

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

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

Aspek

Aspek adalah cara untuk "menyebarkan komputasi ke bawah grafik dependensi". Deskripsi tersebut dijelaskan untuk pengguna Bazel di sini. Contoh yang bagus adalah buffering protokol: aturan proto_library tidak boleh mengetahui bahasa tertentu, tetapi membuat implementasi pesan buffering protokol ("unit dasar" dari buffering protokol) dalam bahasa pemrograman apa pun harus digabungkan ke aturan proto_library, sehingga jika dua target dalam bahasa yang sama bergantung pada buffering protokol yang sama, target tersebut hanya akan dibangun sekali.

Sama seperti target yang dikonfigurasi, target tersebut direpresentasikan di Skyframe sebagai SkyValue dan cara pembentukannya sangat mirip dengan cara membangun target yang dikonfigurasi: target tersebut memiliki class factory bernama ConfiguredAspectFactory yang memiliki akses ke RuleContext, tetapi tidak seperti factory target yang dikonfigurasi, target juga mengetahui target yang dikonfigurasi yang terpasang dan penyedianya.

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

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

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

Kompleksitas aspek pada aspek ditangkap di class AspectCollection.

Platform dan toolchain

Bazel mendukung build multi-platform, yaitu build yang mungkin berisi beberapa arsitektur tempat tindakan build berjalan dan beberapa arsitektur yang kodenya dibangun. 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 pada OS tertentu dan dapat menargetkan beberapa OS lainnya. Bazel harus menentukan compiler C++ yang digunakan berdasarkan eksekusi set dan platform target (dokumentasi untuk toolchain di sini).

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

  1. Aturan toolchain() yang menjelaskan kumpulan batasan eksekusi dan target yang didukung toolchain dan memberi tahu jenis (seperti C++ atau Java) toolchain tersebut (yang terakhir diwakili oleh aturan toolchain_type())
  2. Aturan khusus bahasa yang menjelaskan toolchain 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 dari itu, sehingga membutuhkan lebih banyak waktu untuk dimuat.

Platform eksekusi ditentukan dengan salah satu cara berikut:

  1. Di file WORKSPACE menggunakan fungsi register_execution_platforms()
  2. Pada command line menggunakan opsi command line --extra_execution_platforms

Kumpulan platform eksekusi yang tersedia dikomputasi di RegisteredExecutionPlatformsFunction .

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

Kumpulan toolchain yang akan digunakan untuk target yang dikonfigurasi ditentukan oleh ToolchainResolutionFunction. Ini adalah fungsi dari:

  • Kumpulan toolchain terdaftar (dalam file WORKSPACE 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), di UnloadedToolchainContextKey

Hasilnya adalah UnloadedToolchainContext, yang pada dasarnya merupakan peta dari jenis toolchain (diwakili sebagai instance ToolchainTypeInfo) ke label rantai alat yang dipilih. Alat ini disebut "dibongkar" karena tidak berisi fitur itu sendiri, hanya labelnya.

Kemudian, toolchain benar-benar dimuat menggunakan ResolvedToolchainContext.load() dan digunakan oleh implementasi target yang dikonfigurasi yang memintanya.

Kami juga memiliki sistem lama yang mengandalkan keberadaan satu konfigurasi "host" dan konfigurasi target yang direpresentasikan oleh berbagai flag konfigurasi, seperti --cpu . Kami akan bertransisi secara bertahap ke sistem di atas. Untuk menangani kasus ketika orang mengandalkan nilai konfigurasi lama, kami telah menerapkan pemetaan platform untuk menerjemahkan antara tanda lama dan batasan platform gaya baru. Kodenya menggunakan bahasa PlatformMappingFunction dan menggunakan "bahasa kecil" non-Starlark.

Batasan

Terkadang, seseorang ingin menetapkan target sebagai hanya kompatibel dengan beberapa platform. Sayangnya, Bazel memiliki beberapa mekanisme untuk mencapai tujuan ini:

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

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

environment_group() dan environment()

Aturan ini adalah mekanisme lama dan tidak digunakan secara luas.

Semua aturan build dapat mendeklarasikan "lingkungan" mana yang dapat dibuat, dengan "lingkungan" adalah instance aturan environment().

Ada berbagai cara menentukan lingkungan yang didukung untuk aturan:

  1. Melalui atribut restricted_to=. Ini adalah bentuk spesifikasi paling langsung; cara ini mendeklarasikan kumpulan lingkungan yang tepat yang didukung aturan untuk grup ini.
  2. Melalui atribut compatible_with=. Tindakan 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 aturan environment_group(). Setiap lingkungan dimiliki oleh grup pembanding yang terkait secara tematik (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 yang tidak memiliki atribut tersebut akan mewarisi semua setelan default.
  5. Melalui default class aturan. Tindakan ini akan menggantikan default global untuk semua instance dari class aturan yang diberikan. Ini dapat digunakan, misalnya, untuk membuat semua aturan *_test dapat diuji tanpa setiap instance harus mendeklarasikan kemampuan ini secara eksplisit.

environment() diterapkan sebagai aturan reguler, sedangkan environment_group() adalah subclass 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 timbul karena setiap lingkungan harus mendeklarasikan grup lingkungannya dan setiap grup lingkungan harus mendeklarasikan lingkungan defaultnya.

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

Implementasi pemeriksaan batasan ada di RuleContextConstraintSemantics dan TopLevelConstraintSemantics.

Batasan platform

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

Visibilitas

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

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

Perubahan ini diterapkan di tempat berikut:

  • Antarmuka RuleVisibility mewakili deklarasi visibilitas. Nilai dapat berupa konstanta (sepenuhnya publik atau sepenuhnya pribadi) atau daftar label.
  • Label dapat merujuk ke grup paket (daftar paket yang ditentukan sebelumnya), ke paket secara langsung (//pkg:__pkg__) atau subhierarki paket (//pkg:__subpackages__). Ini berbeda dengan sintaksis command line, yang menggunakan //pkg:* atau //pkg/....
  • Grup paket diterapkan 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 menggabungkan melalui package_group dan 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, menambahkan kumpulannya sendiri, dan menggabungkan kumpulan agregat menjadi penyedia info transitif sehingga target yang dikonfigurasi yang 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 harus berada 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 memiliki 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 memunculkan konsep NestedSet. Ini adalah struktur data yang terdiri dari instance NestedSet lain dan beberapa anggotanya sendiri, sehingga membentuk grafik set asiklik terarah. ID tersebut tidak dapat diubah dan anggotanya dapat diiterasi. Kita mendefinisikan beberapa urutan iterasi (NestedSet.Order): preorder, postorder, topologis (node selalu muncul setelah ancestor-nya) dan "tidak peduli, tetapi harus sama setiap waktu".

Struktur data yang sama disebut depset di Starlark.

Artefak dan Tindakan

Build yang sebenarnya terdiri dari serangkaian perintah yang harus dijalankan untuk menghasilkan output yang diinginkan pengguna. Perintah direpresentasikan sebagai instance class Action dan file direpresentasikan sebagai instance class Artifact. Grafik tersusun dalam grafik bipartit, terarah, dan asiklik yang disebut "grafik tindakan".

Artefak terdiri dari dua jenis: artefak sumber (yang tersedia sebelum Bazel mulai dieksekusi) dan artefak turunan (yang perlu dibangun). Artefak turunan bisa berupa beberapa jenis:

  1. **Artefak reguler. **Keaktualan file ini diperiksa dengan cara menghitung checksumnya, dengan menggunakan mtime sebagai pintasan; kami tidak melakukan checksum pada file jika waktunya belum berubah.
  2. Artefak symlink yang belum di-resolve. Link ini diperiksa untuk menampilkan data terbaru dengan memanggil readlink(). Tidak seperti artefak biasa, symlink ini dapat menjadi symlink yang menjuntai. Biasanya digunakan dalam kasus ketika satu file kemudian mengemas beberapa file ke dalam semacam arsip.
  3. Artefak pohon. Ini bukan file tunggal, melainkan pohon direktori. File diperiksa untuk mengetahui informasi terbaru dengan memeriksa kumpulan file di dalamnya dan kontennya. Class tersebut direpresentasikan sebagai TreeArtifact.
  4. Artefak metadata konstan. Perubahan pada artefak ini tidak memicu build ulang. Ini digunakan secara eksklusif untuk informasi stempel build: kita tidak ingin melakukan pembuatan ulang hanya karena waktu saat ini berubah.

Tidak ada alasan mendasar mengapa artefak sumber tidak boleh berupa artefak pohon atau artefak symlink yang belum terselesaikan, hanya saja kita belum mengimplementasikannya (kita harusnya, -- merujuk direktori sumber dalam file BUILD adalah salah satu beberapa masalah kesalahan yang telah lama ada sejak lama pada Bazel; kami memiliki implementasi semacam pekerjaan yang diaktifkan oleh properti JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1)

Jenis Artifact yang terkenal adalah perantara. Peristiwa tersebut ditunjukkan oleh instance Artifact yang merupakan output MiddlemanAction. Mereka digunakan untuk mengutamakan beberapa hal:

  • Agregat perantara digunakan untuk mengelompokkan artefak bersama-sama. Hal ini dimaksudkan agar jika banyak tindakan menggunakan kumpulan input besar yang sama, kita tidak memiliki tepi dependensi N*M, hanya N+M (keduanya diganti dengan kumpulan bertingkat)
  • Penjadwalan perantara dependensi memastikan bahwa tindakan berjalan sebelum tindakan lainnya. Bahasa ini sebagian besar digunakan untuk analisis lint, tetapi juga untuk kompilasi C++ (lihat CcCompilationContext.createMiddleman() untuk penjelasannya)
  • Mediasi runfiles digunakan untuk memastikan keberadaan hierarki runfile sehingga hierarki tersebut tidak perlu bergantung pada manifes output dan setiap artefak tunggal yang direferensikan oleh hierarki runfiles.

Tindakan paling baik dipahami sebagai perintah yang perlu dijalankan, lingkungan yang dibutuhkan, dan serangkaian output yang dihasilkannya. Hal-hal berikut adalah komponen utama deskripsi tindakan:

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

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

Pada akhirnya, kita ingin memindahkan semuanya ke SpawnAction; JavaCompileAction cukup mendekati, tetapi C++ merupakan kasus khusus karena penguraian file .d dan menyertakan pemindaian.

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

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

Tindakan yang dibagikan

Beberapa tindakan dihasilkan oleh beberapa target yang dikonfigurasi; aturan Starlark lebih terbatas karena hanya diizinkan untuk menempatkan tindakan turunan ke dalam direktori yang ditentukan oleh konfigurasi 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 kesalahan fitur, tetapi menghilangkannya sangat sulit karena menghasilkan penghematan waktu eksekusi yang signifikan saat, misalnya, file sumber perlu diproses dan file tersebut direferensikan oleh beberapa aturan (handwave-handwave). Ini menimbulkan biaya pada beberapa RAM: setiap instance tindakan bersama harus disimpan di memori secara terpisah.

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

Fase eksekusi

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

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

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

  • Tindakan ini mengubah command line tindakan saat paket dipindahkan dari entri jalur paket ke entri lainnya (sebelumnya umum terjadi)
  • Jika suatu tindakan dijalankan dari jarak jauh, prosesnya akan menghasilkan command line yang berbeda
  • Diperlukan transformasi command line khusus untuk alat yang digunakan (pertimbangkan perbedaan antara seperti classpath Java dan jalur penyertaan C++)
  • Mengubah command line suatu tindakan akan membatalkan entri cache tindakannya
  • --package_path secara bertahap dan terus-menerus tidak digunakan lagi

Kemudian, Bazel mulai melintasi 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, kita memiliki beberapa lapisan cache yang dapat di capai di belakang Skyframe:

  • ActionExecutionFunction.stateMap berisi data agar Skyframe memulai ulang ActionExecutionFunction secara murah
  • Cache tindakan lokal berisi data tentang status sistem file
  • Sistem eksekusi jarak jauh biasanya juga berisi cache-nya sendiri

Cache tindakan lokal

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

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

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

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

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

Penemuan input dan pemangkasan input

Beberapa tindakan lebih rumit daripada sekadar memiliki satu set input. Perubahan pada serangkaian input 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 yang matang tentang file header yang digunakan oleh file C++ dari penutupan transitifnya sehingga kita tidak menghendaki 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 menemukan opsi Bazel yang disertakan secara transitif dan hanya menandai file header yang disertakan secara transitif (yang saat ini disebutkan sebagai header Bazel yang berkabel) agar dapat dicantumkan sebagai input yang salah di awal.#include
  • Suatu 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 kejadian, dan untuk menghindari rasa malu karena memiliki inkrementalitas yang lebih buruk daripada Make, Bazel memanfaatkan fakta ini. Kemampuan ini memberikan perkiraan yang lebih baik daripada pemindai include karena bergantung pada compiler.

Hal ini diimplementasikan menggunakan metode pada Tindakan:

  1. Action.discoverInputs() dipanggil. Tindakan ini akan menampilkan sekumpulan Artefak bertingkat yang dinyatakan wajib. Elemen tersebut harus berupa artefak sumber sehingga tidak ada tepi dependensi dalam grafik tindakan yang tidak memiliki nilai yang 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() sehingga kumpulan input tersebut mencerminkan hasil penemuan dan pemangkasan input yang dilakukan sebelumnya.

Tindakan Starlark dapat menggunakan fasilitas ini 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 berbagai cara. Misalnya, command line dapat dijalankan secara lokal, lokal, tetapi dalam berbagai jenis sandbox, atau dari jarak jauh. Konsep yang mewujudkan hal ini disebut ActionContext (atau Strategy, karena kita berhasil melakukan setengah jalan dengan penggantian nama...)

Siklus hidup konteks tindakan adalah sebagai berikut:

  1. Saat fase eksekusi dimulai, instance BlazeModule ditanya tentang konteks tindakan yang mereka miliki. Hal ini terjadi dalam konstruktor ExecutionTool. Jenis konteks tindakan diidentifikasi oleh instance Class Java yang merujuk ke sub-antarmuka ActionContext dan antarmuka yang harus diimplementasikan oleh konteks tindakan.
  2. Konteks tindakan yang sesuai akan dipilih dari tindakan yang tersedia, serta diteruskan ke ActionExecutionContext dan BlazeExecutor .
  3. Tindakan meminta konteks menggunakan ActionExecutionContext.getContext() dan BlazeExecutor.getStrategy() (seharusnya hanya ada satu cara untuk melakukannya...)

Strategi bebas memanggil strategi lain untuk melakukan tugasnya; hal ini digunakan, misalnya, dalam strategi dinamis yang memulai tindakan secara lokal dan jarak jauh, lalu menggunakan penyelesaian mana yang lebih dulu.

Salah satu strategi penting adalah metode yang mengimplementasikan proses pekerja yang persisten (WorkerSpawnStrategy). Idenya adalah beberapa alat memiliki waktu startup yang lama dan oleh karena itu harus digunakan kembali antartindakan, bukan memulai yang baru untuk setiap tindakan (Ini merupakan potensi masalah ketepatan, karena Bazel mengandalkan janji proses pekerja bahwa alat tersebut tidak membawa status yang dapat diamati antara setiap permintaan)

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

Informasi selengkapnya tentang strategi (atau konteks tindakan!):

  • Informasi tentang berbagai strategi untuk menjalankan tindakan tersedia di sini.
  • Informasi tentang strategi dinamis, saat kita menjalankan tindakan baik secara lokal maupun jarak jauh untuk melihat strategi mana yang lebih dulu selesai, tersedia di sini.
  • Informasi tentang seluk-beluk menjalankan tindakan secara lokal tersedia di sini.

Pengelola sumber daya lokal

Bazel dapat menjalankan banyak tindakan secara paralel. Jumlah tindakan lokal yang harus dijalankan secara paralel berbeda dari satu tindakan ke tindakan lainnya: makin banyak resource yang diperlukan suatu tindakan, makin sedikit instance yang harus dijalankan pada saat yang sama untuk menghindari overload mesin lokal.

Hal 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 hingga resource yang diperlukan tersedia.

Deskripsi 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 cara menentukan nama direktori yang terkait dengan konfigurasi tertentu? Ada dua properti yang diinginkan bertentangan:

  1. Jika dua konfigurasi bisa muncul pada build yang sama, konfigurasi tersebut harus memiliki direktori yang berbeda agar keduanya dapat memiliki versinya sendiri untuk tindakan yang sama; jika tidak, jika kedua konfigurasi tidak sepakat mengenai hal seperti command line dari tindakan yang menghasilkan file output yang sama, Bazel tidak akan mengetahui tindakan mana yang harus dipilih ("konflik tindakan")
  2. Jika dua konfigurasi merepresentasikan "kira-kira" hal yang sama, konfigurasi tersebut harus memiliki nama yang sama agar tindakan yang dijalankan di salah satunya dapat digunakan kembali jika command line cocok: misalnya, perubahan pada opsi command line ke compiler Java tidak akan mengakibatkan tindakan kompilasi C++ dijalankan ulang.

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

Pendekatan saat ini adalah bahwa 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 rangkaian transisi konfigurasi Starlark ditambahkan sehingga pengguna tidak dapat menyebabkan konflik tindakan. Ini jauh dari sempurna. Hal 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 melakukan deflaking atau mengumpulkan data pengaturan waktu)
  • Pengujian sharding (membagi kasus pengujian dalam pengujian yang sama melalui beberapa proses untuk kecepatan)
  • Menjalankan kembali pengujian yang tidak stabil
  • Mengelompokkan pengujian ke dalam rangkaian pengujian

Pengujian adalah target yang dikonfigurasi secara reguler yang memiliki TestProvider, yang menjelaskan cara pengujian harus dijalankan:

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

Menentukan pengujian yang akan dijalankan

Menentukan pengujian yang dijalankan adalah proses yang rumit.

Pertama, selama penguraian pola target, rangkaian pengujian diperluas secara rekursif. Perluasan diimplementasikan di TestsForTargetPatternFunction. Kerutan yang agak mengejutkan adalah jika rangkaian pengujian mendeklarasikan tidak ada pengujian, hal ini akan merujuk pada setiap pengujian dalam paketnya. Hal ini diimplementasikan di Package.beforeBuild() dengan menambahkan atribut implisit yang disebut $implicit_tests ke aturan rangkaian pengujian.

Kemudian, pengujian difilter untuk ukuran, tag, waktu tunggu, dan bahasa sesuai dengan opsi command line. Hal ini diimplementasikan 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.

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

Untuk memberikan transparansi pada proses yang rumit ini, operator kueri tests() (diimplementasikan di TestsFunction) tersedia untuk memberi tahu pengujian mana yang dijalankan saat target tertentu ditentukan pada command line. Sayangnya, implementasi ulang tidak dapat diterapkan, sehingga mungkin menyimpang dari yang disebutkan di atas dengan beberapa cara yang tidak begitu kentara.

Menjalankan pengujian

Cara pengujian dijalankan adalah dengan meminta artefak status cache. Tindakan 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 rumit yang menggunakan variabel lingkungan untuk membedakan pengujian dengan apa yang diharapkan darinya. Deskripsi mendetail tentang hal yang diharapkan Bazel dari pengujian dan pengujian yang dapat diharapkan dari Bazel tersedia di sini. Sederhananya, kode keluar 0 berarti berhasil, sedangkan yang lainnya berarti kegagalan.

Selain file status cache, setiap proses pengujian akan mengeluarkan sejumlah file lainnya. Direktori tersebut ditempatkan di "direktori log pengujian" yang merupakan subdirektori bernama testlogs dari direktori output konfigurasi target:

  • test.xml, file XML bergaya JUnit yang menjelaskan setiap kasus pengujian di shard pengujian pengujian
  • test.log, output konsol pengujian. stdout dan stderr tidak dipisah.
  • test.outputs, "direktori output yang 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 dapat terjadi selama membangun target reguler: eksekusi uji eksklusif dan streaming output.

Beberapa pengujian perlu dijalankan dalam mode eksklusif, misalnya tidak paralel dengan pengujian lainnya. Hal ini dapat diperoleh 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 diimplementasikan di SkyframeExecutor.runExclusiveTest().

Tidak seperti tindakan reguler, yang output terminalnya dibuang saat tindakan selesai, pengguna dapat meminta output pengujian untuk di-streaming sehingga 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 berbagai pengujian tidak diselingi.

Hal ini diterapkan di class StreamedTestOutput yang diberi nama dengan tepat dan berfungsi dengan melakukan polling perubahan pada file test.log pengujian yang dimaksud dan memasukkan 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 Build Event Protocol dan dikeluarkan ke konsol oleh AggregatingTestListener.

Pengumpulan 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 sistem menjalankan pengujian. Pengujian sendiri dapat menjalankan beberapa subproses dan terdiri dari bagian yang ditulis dalam berbagai bahasa pemrograman yang berbeda (dengan runtime koleksi cakupan terpisah). Skrip wrapper bertanggung jawab untuk mengonversi file yang dihasilkan ke format LCOV jika diperlukan, dan menggabungkannya menjadi satu file.

Interposisi collect_coverage.sh dilakukan oleh strategi pengujian dan mengharuskan collect_coverage.sh berada pada input pengujian. Hal ini dilakukan oleh atribut implisit :coverage_support yang di-resolve menjadi nilai flag konfigurasi --coverage_support (lihat TestConfiguration.TestOptions.coverageSupport)

Beberapa bahasa melakukan instrumentasi offline, yang berarti instrumentasi cakupan ditambahkan pada waktu kompilasi (seperti C++) dan bahasa 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 jika tidak ada kode yang dijalankan. Masalah yang dipecahkan adalah jika Anda ingin menghitung cakupan pengujian untuk biner, tidak cukup untuk menggabungkan cakupan semua pengujian karena mungkin ada kode dalam biner yang tidak ditautkan ke pengujian apa pun. Oleh karena itu, yang kami lakukan adalah memunculkan file cakupan untuk setiap biner yang hanya berisi file yang cakupannya kami kumpulkan tanpa baris yang dicakup. File cakupan dasar pengukuran untuk target adalah ukuran bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat . Flag ini juga dibuat untuk biner dan library, selain untuk pengujian apakah Anda meneruskan flag --nobuild_tests_only ke Bazel.

Cakupan dasar saat ini rusak.

Kami melacak dua grup file untuk pengumpulan cakupan bagi setiap aturan: kumpulan file instrumentasi dan kumpulan file metadata instrumentasi.

Hanya itu kumpulan file berinstrumen, yaitu kumpulan file untuk diinstrumentasikan. Untuk runtime cakupan online, hal ini dapat digunakan saat runtime untuk menentukan file mana yang akan diinstrumentasikan. Cakupan juga digunakan untuk menerapkan cakupan dasar.

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

Apakah cakupan sedang 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, sehingga sedikit mengurangi masalah ini, karena analisis ulang tetap diperlukan).

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

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

Mesin kueri

Bazel memiliki bahasa kecil 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 mengizinkan hasil kueri streaming, alih-alih mengumpulkannya ke beberapa struktur data, query2.engine.Callback diteruskan ke QueryFunction, yang memanggilnya untuk hasil yang ingin ditampilkan.

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

Persyaratan halus dari beberapa format output kueri (tentunya adalah proto) adalah Bazel perlu memunculkan _semua _informasi yang disediakan oleh pemuatan paket sehingga Anda dapat melakukan diff pada 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 umum adalah menggunakan label, dan melampirkan informasi yang kompleks ke aturan dengan label tersebut. Ini bukan solusi yang sangat memuaskan dan akan sangat baik untuk mencabut persyaratan ini.

Sistem modul

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

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

  • Antarmuka ke sistem eksekusi jarak jauh
  • Perintah baru

Rangkaian titik ekstensi yang ditawarkan BlazeModule agak berbahaya. Jangan menggunakannya sebagai contoh prinsip desain yang baik.

{i>Bus<i} acara

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

  • Daftar target build yang akan di-build telah ditentukan (TargetParsingCompleteEvent)
  • Konfigurasi tingkat teratas telah ditentukan (BuildConfigurationEvent)
  • Target dibuat, berhasil atau tidak (TargetCompleteEvent)
  • Pengujian 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. Dapat diakses sebagai file yang berisi pesan protokol, atau Bazel dapat terhubung ke server (disebut Build Event Service) untuk melakukan streaming peristiwa.

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

Repositori eksternal

Meskipun Bazel awalnya dirancang untuk digunakan dalam monorepo (pohon sumber tunggal yang berisi semua yang diperlukan untuk membangun), Bazel tinggal di dunia yang belum tentu berlaku. "Repositori eksternal" adalah abstraksi yang digunakan untuk menjembatani dua dunia ini: keduanya mewakili kode yang diperlukan untuk build, tetapi tidak berada dalam hierarki sumber utama.

File WORKSPACE

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

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

Menghasilkan repositori bernama @foo tersedia. Permasalahannya adalah 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) dipecah menjadi beberapa bagian yang ditandai 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. Hal ini menyebabkan Bazel membuat direktori dalam $OUTPUT_BASE/external/<repository name>.

Mengambil repositori dilakukan dalam langkah-langkah berikut:

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

Ada berbagai lapisan caching karena mengambil repositori bisa sangat mahal:

  1. Ada cache untuk file yang didownload yang terkunci oleh checksum-nya (RepositoryCache). Hal ini mengharuskan checksum tersedia dalam file WORKSPACE, tetapi tetap bagus untuk Hermeticity. Hal ini digunakan bersama oleh setiap instance server Bazel di workstation yang sama, terlepas dari ruang kerja atau basis output tempat mereka berjalan.
  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, data 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 kemutakhiran artefak sumber dengan memanggil stat(), dan artefak ini juga tidak divalidasi saat definisi repositori yang diubah. Dengan demikian, FileStateValue untuk artefak dalam repositori eksternal harus bergantung pada repositori eksternalnya. Hal ini ditangani oleh ExternalFilesHelper.

Direktori terkelola

Terkadang, repositori eksternal perlu mengubah file pada root Workspace (seperti pengelola paket yang menampung paket yang didownload dalam subdirektori hierarki sumber). Hal ini bertentangan dengan asumsi Bazel membuat file sumber hanya diubah oleh pengguna dan bukan oleh pengguna itu sendiri, serta memungkinkan paket untuk merujuk ke setiap direktori dalam root ruang kerja. Agar repositori eksternal semacam ini berfungsi, Bazel melakukan dua hal:

  1. Memungkinkan pengguna menentukan subdirektori ruang kerja yang tidak boleh dijangkau Bazel. Fungsi tersebut tercantum dalam file bernama .bazelignore dan fungsinya diimplementasikan di BlacklistedPackagePrefixesFunction.
  2. Kami mengenkode pemetaan dari subdirektori ruang kerja ke repositori eksternal yang ditangani menjadi ManagedDirectoriesKnowledge dan menangani FileStateValue yang merujuknya dengan cara yang sama seperti repositori untuk repositori eksternal reguler.

Pemetaan repositori

Bisa saja beberapa repositori ingin bergantung pada repositori yang sama, tetapi dalam versi yang berbeda (ini adalah instance "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 dengan @guava// dan memperkirakan bahwa hal tersebut 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//) dalam repositori satu biner dan repositori Guava lainnya (seperti @guava2//) repositori yang lain.

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

Pemetaan ditentukan dalam file WORKSPACE sebagai atribut repo_mapping dari definisi repositori individual. Kemudian, objek ini akan muncul di Skyframe sebagai anggota WorkspaceFileValue, yang ditautkan 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 diurai dalam fase pemuatan)
  • BzlLoadFunction untuk me-resolve label dalam pernyataan load()

Bit JNI

Server Bazel_ sebagian besar _ditulis dalam bahasa Java. Pengecualiannya adalah bagian yang tidak dapat dilakukan Java sendiri atau tidak dapat dilakukan dengan sendirinya 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 bagian src/main/native dan class Java dengan metode native:

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

Output konsol

Memancarkan output konsol tampaknya adalah hal yang sederhana, tetapi pertemuan untuk menjalankan beberapa proses (terkadang jarak jauh), caching yang terperinci, keinginan untuk memiliki output terminal yang bagus dan penuh warna, serta memiliki server yang berjalan lama membuatnya menjadi tidak mudah.

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

Output secara default dicetak dengan urutan escape ANSI. Jika tidak diinginkan (--color=no), fungsi ini akan dihilangkan oleh AnsiStrippingOutputStream. Selain itu, System.out dan System.err akan dialihkan ke aliran output ini. Hal ini dimaksudkan agar informasi proses debug dapat dicetak menggunakan System.err.println() dan tetap berakhir di output terminal klien (yang berbeda dengan output server). Perlu diperhatikan bahwa jika 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, keduanya berbeda dengan postingan yang dikirim ke EventBus (ini membingungkan). Setiap Event memiliki EventKind (error, peringatan, info, dan beberapa lainnya) serta mungkin memiliki Location (tempat dalam kode sumber yang menyebabkan peristiwa terjadi).

Beberapa implementasi EventHandler menyimpan peristiwa yang diterimanya. 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 sampai 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 harus mengimplementasikan antarmuka ini; hanya peristiwa yang di-cache oleh ExtendedEventHandler (akan lebih baik dan sebagian besar hal 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. Kode ini memiliki dua input:

  • {i>Bus<i} acara
  • Aliran peristiwa ini disalurkan melalui Reporter

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

Membuat Profil Bazel

Bazel adalah perangkat yang cepat. Bazel juga lambat, karena build cenderung tumbuh sampai hanya pada bagian yang bisa ditanggung. Karena alasan ini, Bazel menyertakan profiler yang dapat digunakan untuk membuat profil build dan Bazel itu sendiri. Hal ini diimplementasikan di class yang diberi nama dengan tepat Profiler. Fungsi ini diaktifkan secara default, meskipun hanya merekam data ringkas sehingga overhead-nya dapat ditoleransi; command line --record_full_profiler_data membuatnya merekam semua yang dapat dilakukannya.

Profil ini menampilkan profil dalam format profiler Chrome; dan paling baik dilihat di Chrome. Model datanya adalah stack tugas: satu 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 masing-masing dalam BlazeRuntime.initProfiler() dan BlazeRuntime.afterCommand(), serta berupaya aktif selama mungkin agar kita dapat membuat profil semuanya. Untuk menambahkan sesuatu ke profil, panggil Profiler.instance().profile(). Metode ini menampilkan Closeable, yang penutupannya mewakili akhir tugas. Tabel ini paling baik digunakan dengan pernyataan coba dengan sumber daya.

Kita juga melakukan pembuatan profil memori dasar di MemoryProfiler. Fungsi ini juga selalu aktif dan sebagian besar mencatat 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 yang pertama sebagai "pengujian integrasi" dan yang terakhir sebagai "pengujian unit", meskipun pengujian ini lebih mirip dengan pengujian integrasi yang kurang terintegrasi. Kami juga memiliki beberapa pengujian unit yang sebenarnya, yang diperlukan.

Pengujian integrasi memiliki dua jenis:

  1. Yang diimplementasikan menggunakan framework pengujian bash yang sangat rumit di bawah src/test/shell
  2. One diimplementasikan di Java. Argumen ini diterapkan sebagai subclass BuildIntegrationTestCase

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

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