Codebase Bazel

Laporkan masalah Lihat sumber Per malam · 7,2 · 7,1 · 7,0 · 6,5 · 6,4

Dokumen ini adalah deskripsi codebase dan struktur Bazel. Ini ditujukan bagi orang-orang yang bersedia berkontribusi pada Bazel, bukan untuk pengguna akhir.

Pengantar

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

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

Versi publik kode sumber Bazel terdapat pada GitHub di github.com/bazelbuild/bazel. Ini bukan "sumber kebenaran"; itu berasal dari pohon sumber internal Google yang berisi fungsi tambahan yang tidak berguna di luar Google. Tujuan tujuan jangka panjangnya adalah menjadikan GitHub sebagai sumber kebenaran.

Kontribusi diterima melalui mekanisme permintaan pull GitHub biasa, dan diimpor secara manual oleh seorang Googler ke dalam hierarki sumber internal, lalu diekspor ulang 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.

Inilah mengapa baris perintah Bazel memiliki dua jenis opsi: {i>startup<i} dan perintah. Dalam command line seperti ini:

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

Beberapa opsi (--host_jvm_args=) ditempatkan sebelum nama perintah yang akan dijalankan dan beberapa setelah (-c opt); jenis yang pertama disebut “opsi {i>startup<i}” dan mempengaruhi proses server secara keseluruhan, sedangkan jenis yang terakhir, perintah "opsi", hanya memengaruhi satu perintah.

Setiap instance server memiliki satu ruang kerja terkait (kumpulan sumber yang dikenal sebagai "repositori") dan setiap ruang kerja biasanya memiliki satu tempat aktif di instance server tertentu. 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 merupakan file .zip yang valid. Saat Anda mengetik bazel, file ELF yang dapat dieksekusi di atas diimplementasikan dalam C++ ( "klien") mendapatkan kontrol. Klien menyiapkan proses server yang sesuai dengan menggunakan langkah-langkah berikut:

  1. Memeriksa apakah aplikasi telah mengekstrak dirinya sendiri. Jika tidak, sistem akan melakukannya. Ini adalah asal implementasi server.
  2. Memeriksa apakah ada {i>instance<i} server aktif yang berfungsi: sedang berjalan, memiliki opsi startup yang tepat dan menggunakan direktori ruang kerja yang tepat. Ini menemukan server yang sedang berjalan dengan melihat direktori $OUTPUT_BASE/server di mana ada file kunci dengan porta 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 adalah dikomunikasikan kepadanya melalui antarmuka gRPC, maka {i>output<i} Bazel disalurkan kembali ke terminal. Hanya satu perintah yang dapat dijalankan secara bersamaan. Ini adalah diimplementasikan menggunakan mekanisme penguncian yang rumit dengan bagian-bagian di C++ dan bagian-bagian Java. Ada beberapa infrastruktur untuk menjalankan beberapa perintah secara paralel, karena ketidakmampuan untuk menjalankan bazel version secara paralel dengan perintah lain agak memalukan. Penghalang utama adalah siklus proses BlazeModule dan beberapa status di BlazeRuntime.

Pada akhir suatu perintah, server Bazel mengirimkan kode keluar klien akan ditampilkan. Masalah yang menarik adalah implementasi bazel run: perintah ini adalah menjalankan sesuatu yang baru saja dibangun Bazel, tetapi ia tidak dapat melakukannya dari proses server karena tidak memiliki terminal. Jadi, sebaliknya, memberitahu kepada klien mengenai biner apa yang seharusnya ujexec() dan argumennya.

Saat seseorang menekan Ctrl-C, klien akan menerjemahkannya menjadi panggilan Cancel di gRPC {i>fiber<i}, yang mencoba menghentikan perintah tersebut sesegera mungkin. Setelah Ctrl-C ketiga, klien mengirimkan SIGKILL ke server sebagai gantinya.

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 gRPC memanggil dari klien akan ditangani oleh GrpcServerImpl.run().

Tata letak direktori

Bazel membuat serangkaian direktori yang agak rumit selama proses build. Penuh tersedia di Tata letak direktori output.

"Repo utama" adalah pohon sumber yang menjalankan Bazel. Biasanya sesuai dengan sesuatu yang Anda periksa dari {i>source control<i}. {i>Root<i} dari direktori ini adalah yang dikenal sebagai "root ruang kerja".

Bazel meletakkan semua datanya di bawah "root pengguna {i>output<i}". Hal ini biasanya $HOME/.cache/bazel/_bazel_${USER}, tetapi dapat diganti menggunakan Opsi startup --output_user_root.

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

"Basis output" tempat instance Bazel terhubung ke ke ruang kerja penerima. 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 mulai --output_base, yaitu, antara lain, berguna untuk mengatasi keterbatasan yang hanya satu instance Bazel dapat berjalan di ruang kerja mana pun pada waktu tertentu.

Direktori output berisi, antara lain:

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

Proses mengeksekusi perintah

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

  1. BlazeCommandDispatcher akan diberi tahu tentang permintaan baru tersebut. Hal itu memutuskan apakah perintah membutuhkan ruang kerja untuk dijalankan (hampir setiap perintah kecuali bagi file yang tidak ada hubungannya dengan kode sumber, seperti versi atau help) dan apakah perintah lain sedang berjalan.

  2. Perintah yang tepat ditemukan. Setiap perintah harus mengimplementasikan antarmuka BlazeCommand dan harus memiliki anotasi @Command (ini sedikit anti-pola, akan lebih baik jika semua {i>metadata<i} yang dibutuhkan sebuah perintah dijelaskan oleh metode pada BlazeCommand)

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

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

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

  6. Kumpulan pola target pada baris perintah diurai dan menggunakan karakter pengganti seperti //pkg:all dan //pkg/... telah diselesaikan. Hal ini diimplementasikan di AnalysisPhaseRunner.evaluateTargetPatterns() dan ditetapkan ulang di Skyframe sebagai TargetPatternPhaseValue.

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

  8. Fase eksekusi dijalankan. Ini berarti menjalankan setiap tindakan yang diperlukan untuk membangun target tingkat atas yang diminta akan dijalankan.

Opsi command line

Opsi baris perintah untuk pemanggilan Bazel dijelaskan dalam Objek OptionsParsingResult, yang kemudian berisi peta dari "option kelas" pada nilai-nilai opsi. "Class opsi" adalah subclass dari OptionsBase dan mengelompokkan opsi command line yang berkaitan dengan masing-masing lainnya. Contoh:

  1. Opsi yang terkait dengan bahasa pemrograman (CppOptions atau JavaOptions). Class ini harus berupa subclass FragmentOptions dan pada akhirnya digabungkan menjadi 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 atau ctx.fragments di Starlark). Beberapa di antaranya (misalnya, apakah C++ menyertakan pemindaian atau tidak) sudah dibaca di fase eksekusi, tetapi hal itu selalu membutuhkan pipa eksplisit karena BuildConfiguration tidak tersedia pada saat itu. Untuk informasi selengkapnya, lihat bagian "Konfigurasi".

PERINGATAN: Kita ingin berpura-pura bahwa instance OptionsBase tidak dapat diubah dan menggunakannya seperti itu (misalnya bagian dari SkyKeys). Tidak seperti itu dan memodifikasinya adalah cara yang sangat bagus untuk menghancurkan Bazel dengan cara halus yang sulit untuk men-debug. Sayangnya, membuatnya benar-benar tidak dapat diubah adalah upaya besar. (Mengubah FragmentOptions segera setelah dibuat sebelum orang lain mendapatkan kesempatan untuk menyimpan referensi ke nama tersebut 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 di setiap perintah Bazel
  3. Dari ConfiguredRuleClassProvider (ini adalah opsi command line yang terkait ke bahasa pemrograman individu)
  4. Aturan Starlark juga dapat menentukan opsi mereka sendiri (lihat di sini)

Setiap opsi (tidak termasuk opsi yang ditentukan Starlark) adalah variabel anggota dari Subclass FragmentOptions yang memiliki anotasi @Option, yang menentukan nama dan jenis opsi baris perintah beserta beberapa teks bantuan.

Jenis Java dari nilai opsi baris perintah biasanya merupakan sesuatu yang sederhana (string, bilangan bulat, Boolean, label, dll.). Namun, kami juga mendukung opsi jenis yang lebih rumit; dalam hal ini, tugas mengonversi dari {i>string <i}baris perintah ke tipe data akan diturunkan ke implementasi dari com.google.devtools.common.options.Converter.

Pohon sumber, seperti yang terlihat oleh Bazel

Bazel terlibat dalam bisnis membangun perangkat lunak, yang terjadi dengan membaca dan menafsirkan kode sumber. Totalitas kode sumber yang dioperasikan Bazel disebut "ruang kerja" dan ia disusun ke dalam repositori, paket, dan aturan.

Repositori

"Repositori" merupakan pohon sumber tempat pengembang bekerja; hal tersebut biasanya mewakili satu proyek. Leluhur Bazel, Blaze, mengoperasikan komputer monorepo, yaitu, satu pohon sumber yang berisi semua kode sumber yang digunakan untuk menjalankan build. Sebaliknya, Bazel mendukung proyek yang kode sumbernya mencakup repositori tambahan. Repositori tempat Bazel dipanggil disebut "main repositori", yang lainnya disebut "repositori eksternal".

Repositori ditandai oleh file batas repo (MODULE.bazel, REPO.bazel, atau dalam konteks lama, WORKSPACE atau WORKSPACE.bazel) di direktori root-nya. Tujuan repo utama adalah {i>source tree<i} tempat Anda memanggil Bazel. Repositori eksternal didefinisikan dalam berbagai cara; lihat dependensi eksternal ringkasan untuk mengetahui informasi selengkapnya.

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

Saat menjalankan build, seluruh hierarki sumber perlu disatukan; ingin dilakukan oleh SymlinkForest, yang membuat symlink setiap paket di 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. Ini ditentukan oleh file yang disebut BUILD atau BUILD.bazel. Jika keduanya ada, Bazel akan lebih memilih BUILD.bazel; alasan mengapa file BUILD masih diterima adalah karena nenek moyang Bazel, Blaze, menggunakan ini nama file. Namun, ternyata segmen jalur ini umum digunakan, terutama di Windows, di mana nama file tidak peka huruf besar/kecil.

Paket tidak saling bergantung: perubahan pada file BUILD paket tidak dapat menyebabkan paket lain berubah. Penambahan atau penghapusan BUILD file _can _change paket lain, karena glob rekursif berhenti di batas paket dan dengan demikian, keberadaan file BUILD akan menghentikan rekursi.

Evaluasi file BUILD disebut "pemuatan paket". Diimplementasikan di class PackageFactory, bekerja dengan memanggil penafsir Starlark dan memerlukan pengetahuan tentang kumpulan class aturan yang tersedia. Hasil paket pemuatan adalah objek Package. Umumnya, ini adalah peta dari string (nama dari 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 (misalnya glob(["**/*.java"])). Tidak seperti {i>shell<i}, ia mendukung glob rekursif yang turun ke subdirektori (tetapi bukan ke dalam sub-paket). Hal ini memerlukan akses ke sistem file dan karena itu mungkin lambat, kami menerapkan segala macam trik untuk membuatnya berjalan 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 sendiri berisi beberapa anggota yang digunakan secara eksklusif untuk uraikan "eksternal" paket (terkait dengan dependensi eksternal) dan yang tidak masuk akal untuk paket nyata. Ini adalah cacat desain karena objek yang menjelaskan paket biasa tidak boleh mengandung yang mendeskripsikan hal lain. Ini mencakup:

  • Pemetaan repositori
  • Toolchain terdaftar
  • Platform eksekusi yang terdaftar

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

Label, Target, dan Aturan

Paket terdiri dari target yang memiliki jenis berikut:

  1. File: hal-hal yang merupakan input atau output build. Di beberapa Dalam istilah Bazel, kami menyebutnya artefak (dibahas di bagian lain). Tidak semua file yang dibuat selama build adalah target; adalah hal biasa untuk {i>output<i} Bazel tidak boleh memiliki label terkait.
  2. Aturan: ini menjelaskan langkah-langkah untuk memperoleh output-nya dari inputnya. Mereka 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. {i>Syntax<i} label adalah @repo//pac/kage:name, dengan repo adalah nama repositori tempat Label di dalam, pac/kage adalah direktori tempat file BUILD-nya berada dan name adalah jalur file (jika label mengacu pada file sumber) secara relatif terhadap direktori dari paket. Saat merujuk ke target pada baris perintah, ada beberapa bagian label dapat dihilangkan:

  1. Jika repositori dihilangkan, label akan ditempatkan di file utama repositori resource.
  2. Jika bagian paket dihilangkan (seperti name atau :name), label akan digunakan berada dalam paket direktori kerja saat ini (jalur relatif berisi referensi tingkat tinggi (..) tidak diizinkan)

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

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

Class aturan berisi informasi seperti:

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

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

Rangka Langit

Kerangka kerja evaluasi yang mendasari Bazel disebut Skyframe. Modelnya adalah semua yang perlu dibangun selama pembangunan diatur ke dalam sebuah grafik asiklik dengan ujung-ujung yang menunjuk dari bagian data apa pun ke dependensinya, yaitu, potongan data lain yang perlu diketahui untuk menyusunnya.

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

Cara paling mudah untuk mengamati grafik Skyframe adalah dengan menjalankan bazel dump --skyframe=deps, yang menyimpan grafik, satu SkyValue per baris. Yang terbaik melakukannya untuk build kecil, karena bisa menjadi cukup besar.

Skyframe tersedia dalam paket com.google.devtools.build.skyframe. Tujuan paket com.google.devtools.build.lib.skyframe dengan nama serupa berisi Bazel di atas Skyframe. Informasi selengkapnya tentang Skyframe dapat tersedia di sini.

Untuk mengevaluasi SkyKey tertentu menjadi SkyValue, Skyframe akan memanggil metode SkyFunction yang sesuai dengan jenis kunci. Selama evaluasi, sistem dapat meminta dependensi lain dari Skyframe dengan memanggil berbagai overload dari SkyFunction.Environment.getValue(). Opsi ini memiliki efek samping pendaftaran dependensi tersebut ke dalam grafik internal Skyframe, yang diketahui Skyframe untuk mengevaluasi kembali fungsi ketika salah satu dependensinya berubah. Dengan kata lain, {i>caching<i} dan komputasi inkremental Skyframe bekerja di perincian SkyFunction dan SkyValue.

Setiap kali SkyFunction meminta dependensi yang tidak tersedia, getValue() akan mengembalikan nol. Fungsi ini kemudian akan mengembalikan kontrol ke Skyframe dengan itu sendiri mengembalikan nol. Di lain waktu, Skyframe akan mengevaluasi dependensi yang tidak tersedia, lalu mulai ulang fungsi dari awal — hanya kali panggilan getValue() akan berhasil dengan hasil non-null.

Konsekuensi dari hal ini adalah setiap komputasi yang dilakukan di dalam SkyFunction sebelum {i>restart <i}harus diulangi. Tapi ini tidak termasuk pekerjaan yang dilakukan untuk mengevaluasi dependensi SkyValues, yang di-cache. Oleh karena itu, kita umumnya mengerjakan terkait masalah ini dengan:

  1. Mendeklarasikan dependensi dalam batch (dengan menggunakan getValuesAndExceptions()) untuk membatasi jumlah {i>restart<i}.
  2. Memecah SkyValue menjadi bagian-bagian terpisah yang dikomputasi oleh berbagai SkyFunction, sehingga dapat dikomputasi dan di-cache secara terpisah. Ini harus dilakukan secara strategis, karena memiliki potensi untuk meningkatkan memori tingkat penggunaan.
  3. Menyimpan status di antara mulai ulang, baik menggunakan SkyFunction.Environment.getState(), atau mempertahankan cache statis ad hoc "di belakang Skyframe". Dengan SkyFunctions yang kompleks, pengelolaan status antar {i>restart <i}bisa jadi sangat rumit, jadi StateMachine diperkenalkan untuk pendekatan terstruktur terhadap konkurensi logis, termasuk hook untuk menangguhkan dan melanjutkan komputasi hierarkis dalam SkyFunction. Contoh: DependencyResolver#computeDependencies menggunakan StateMachine dengan getState() untuk menghitung kumpulan yang berpotensi besar dependensi langsung dari target yang dikonfigurasi, yang dalam kondisi lain dapat mengakibatkan proses mulai ulang yang mahal.

Pada dasarnya, Bazel membutuhkan solusi semacam ini karena ratusan ribuan simpul Skyframe yang sedang terbang adalah hal umum, dan dukungan Java untuk thread ringan tidak mengungguli implementasi StateMachine sejak tahun 2023.

Starlark

Starlark adalah bahasa khusus domain yang digunakan orang untuk mengkonfigurasi dan memperluas Bazel. Ia dipahami sebagai {i>subset<i} terbatas dari Python yang memiliki tipe yang jauh lebih sedikit, lebih banyak batasan pada alur kontrol, dan yang paling penting, ketetapan yang kuat jaminan untuk memungkinkan pembacaan serentak. Ini belum menyelesaikan Turing, mencegah beberapa (tetapi tidak semua) pengguna untuk mencoba mencapai dari tugas pemrograman dalam bahasa tersebut.

Starlark diimplementasikan dalam paket net.starlark.java. Alat ini juga memiliki implementasi Go independen di sini. Java implementasi yang digunakan dalam Bazel saat ini adalah penerjemah.

Starlark digunakan dalam beberapa konteks, antara lain:

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

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

Informasi selengkapnya tentang Starlark tersedia di sini.

Fase pemuatan/analisis

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

Hal ini disebut "fase pemuatan/analisis" karena dapat dibagi menjadi dua bagian yang berbeda, yang dulunya diserialisasi, tetapi sekarang bisa tumpang tindih dalam waktu:

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

Setiap target yang dikonfigurasi dalam penutupan transitif target yang dikonfigurasi diminta pada baris perintah harus dianalisis dari bawah ke atas; yaitu, node daun pertama, lalu ke baris perintah. Input untuk analisis satu target yang dikonfigurasi adalah:

  1. Konfigurasi. ("bagaimana" untuk membuat aturan itu; misalnya, target tetapi juga hal-hal seperti opsi baris perintah yang diinginkan pengguna diteruskan ke compiler C++)
  2. Dependensi langsung. Penyedia info transitif mereka tersedia pada aturan yang dianalisis. Mereka disebut seperti itu karena menyediakan "gabungkan" informasi dalam penutupan transitif konfigurasi tertentu, seperti semua file .jar di classpath atau semua file .o yang harus ditautkan ke biner C++)
  3. Targetnya sendiri. Ini adalah hasil dari pemuatan paket yang ditargetkan berada. Untuk aturan, hal ini mencakup atributnya, yang biasanya merupakan itu penting.
  4. Implementasi target yang dikonfigurasi. Untuk aturan, parameter ini dapat dalam Starlark atau Java. Semua target yang dikonfigurasi tanpa aturan diterapkan pada Java.

Output dari menganalisis target yang dikonfigurasi adalah:

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

API yang ditawarkan ke aturan Java adalah RuleContext, yang setara dengan Argumen ctx aturan Starlark. API-nya lebih canggih, tetapi pada saat yang sama lebih mudah, lebih mudah untuk melakukan Bad ThingsTM, misalnya untuk menulis kode yang waktu atau kompleksitas ruang yang kuadrat (atau lebih buruk), untuk membuat server Bazel error dengan Pengecualian Java atau untuk melanggar invarian (misalnya dengan secara tidak sengaja memodifikasi Options atau dengan membuat target yang dikonfigurasi dapat berubah)

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

Konfigurasi

Konfigurasi adalah "bagaimana" dalam membangun target: untuk platform apa dan opsi baris perintah, dll.

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

Secara konseptual, konfigurasi adalah instance BuildOptions. Namun, di , BuildOptions digabungkan oleh BuildConfiguration yang memberikan berbagai fungsi tambahan. Ia merambah dari atas grafik dependensi ke bawah. Jika berubah, build harus dianalisis ulang.

Hal ini menyebabkan anomali seperti harus menganalisis ulang seluruh build jika, jumlah pengujian yang diminta berubah, meskipun hanya memengaruhi target pengujian (kami berencana untuk "memangkas" konfigurasi agar hal ini bukan itu masalahnya, tapi belum siap).

Saat penerapan aturan memerlukan bagian dari konfigurasi, penerapan harus mendeklarasikan nya dalam definisinya menggunakan RuleClass.Builder.requiresConfigurationFragments() kami. 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 tidak perlu dianalisis ulang.

Konfigurasi aturan tidak harus sama dengan konfigurasi "induk" aturan. Proses mengubah konfigurasi dalam tepi dependensi disebut "transisi konfigurasi". Hal ini dapat terjadi di dua tempat:

  1. Di edge dependensi. Transisi ini ditetapkan dalam Attribute.Builder.cfg() dan merupakan fungsi dari Rule (dengan terjadi) dan BuildOptions (konfigurasi asli) ke satu atau lebih BuildOptions (konfigurasi output).
  2. Di setiap edge masuk ke target yang dikonfigurasi. Hal ini ditetapkan 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 harus dibangun dalam arsitektur eksekusi
  2. Untuk mendeklarasikan bahwa dependensi tertentu harus dibangun untuk beberapa arsitektur (seperti untuk kode native di APK Android yang gemuk)

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

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

Penyedia info transitif

Penyedia info transitif adalah cara (dan _satu-satunya _way) untuk target yang dikonfigurasi untuk memberi tahu hal-hal tentang target lain yang dikonfigurasi yang bergantung padanya. Alasannya "transitif" dalam namanya adalah bahwa ini biasanya semacam penggabungan penutupan transitif dari target yang dikonfigurasi.

Umumnya ada korespondensi 1:1 antara penyedia info transitif Java dan Starlark (pengecualiannya adalah DefaultInfo yang merupakan penggabungan dari FileProvider, FilesToRunProvider, dan RunfilesProvider karena API tersebut dianggap lebih 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 dari TransitiveInfoProvider.
  2. Sebuah {i>string<i}. Hal ini merupakan warisan dan sangat tidak diinginkan karena rentan terhadap bentrok nama. Penyedia info transitif tersebut adalah subclass langsung dari build.lib.packages.Info .
  3. Simbol penyedia. Ini dapat dibuat dari Starlark menggunakan provider() dan merupakan cara yang direkomendasikan untuk membuat penyedia baru. Simbolnya adalah diwakili oleh instance Provider.Key di Java.

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

Target yang dikonfigurasi

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

Factory target yang dikonfigurasi harus menggunakan RuleConfiguredTargetBuilder untuk mengkonstruksi nilai return. Kode ini terdiri dari hal-hal berikut:

  1. filesToBuild miliknya, konsep buram "kumpulan file aturan ini diwakili oleh variabel tersebut." Ini adalah file yang dibangun 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 lainnya" aturan tersebut bisa buat. Mereka 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". J "pohon runfiles" adalah pohon direktori dari file data untuk biner tertentu. Symlink dibuat di sistem file sebagai pohon symlink dengan masing-masing symlink yang menunjuk ke file dalam sumber hierarki output.

Kumpulan runfile direpresentasikan sebagai instance Runfiles. Secara konseptual, ini adalah memetakan dari jalur file dalam hierarki runfiles ke instance Artifact yang mewakilinya. Ini sedikit lebih rumit daripada satu Map untuk 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 {i>runfile<i}, yang juga memerlukan untuk direpresentasikan.

Runfile dikumpulkan menggunakan RunfilesProvider: instance dari class ini merepresentasikan runfiles target yang dikonfigurasi (seperti library) dan resource transitifnya kebutuhan {i>closure<i} dan mereka dikumpulkan seperti satu set yang bersarang (pada kenyataannya, mereka diimplementasikan menggunakan set bertingkat di bawah penutup): setiap target menggabungkan runfile dependensinya, menambahkan beberapa dependensinya sendiri, lalu mengirimkan set yang dihasilkan di grafik dependensi. Instance RunfilesProvider berisi dua Runfiles instance, satu ketika aturan bergantung pada melalui "data" dan satu untuk setiap jenis dependensi lain yang masuk. Hal ini karena target terkadang menghadirkan runfile yang berbeda jika bergantung pada atribut data dibandingkan sebaliknya. Ini adalah perilaku lama yang tidak diinginkan dan belum kami lewatkan belum dihapus.

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

  • Manifes runfiles input. Ini adalah deskripsi berseri dari hierarki runfiles. Fungsi ini digunakan sebagai {i>proxy<i} untuk konten hierarki runfiles dan Bazel beranggapan bahwa pohon runfile berubah jika dan hanya jika isinya dari perubahan manifes.
  • Manifes runfiles output. Ini digunakan oleh library runtime yang menangani pohon {i>runfile<i}, terutama di Windows, yang terkadang tidak mendukung {i>symbolic link<i}.
  • Perantara runfile. Agar pohon {i>runfiles<i} ada, kita perlu untuk membangun pohon symlink dan artefak yang dituju symlink. Secara berurutan untuk mengurangi jumlah tepi dependensi, {i> middleman<i} runfiles dapat yang digunakan untuk mewakili semua ini.
  • Argumen command line untuk menjalankan biner yang runfile-nya yang diwakili oleh objek RunfilesSupport.

Aspek

Aspek adalah cara untuk "menyebarkan komputasi ke bawah grafik dependensi". Mereka adalah dijelaskan untuk pengguna Bazel di sini. Bagus contoh yang memotivasi adalah buffering protokol: aturan proto_library tidak boleh diketahui tentang bahasa tertentu, tetapi membangun implementasi dari pesan penyangga ("unit dasar" penyangga protokol) dalam pemrograman apa pun bahasa harus digabungkan dengan aturan proto_library, sehingga jika dua target dalam bahasa yang sama tergantung pada {i>buffer<i} protokol yang sama, hanya akan dibangun sekali.

Sama seperti target yang dikonfigurasi, target tersebut direpresentasikan di Skyframe sebagai SkyValue dan cara pembuatannya sangat mirip dengan cara target yang dikonfigurasi dibangun: memiliki kelas factory bernama ConfiguredAspectFactory yang memiliki akses ke RuleContext, tetapi tidak seperti factory target yang dikonfigurasi, alat ini juga mengetahui tentang target terkonfigurasi yang terpasang dan penyedianya.

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

  1. AspectClass adalah implementasi aspek. Bisa dalam bentuk Java (dalam hal ini itu adalah {i>subclass<i}) atau dalam Starlark (dalam hal ini itu adalah instance StarlarkAspectClass). Ini dapat dianalisa dengan RuleConfiguredTargetFactory.
  2. AspectDefinition adalah definisi aspek; model ini mencakup penyedia yang dibutuhkan, penyedia yang disediakan, dan berisi referensi ke implementasinya, seperti instance AspectClass yang sesuai. Penting setara 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 kegunaannya adalah buffering protokol: jika suatu bahasa memiliki beberapa API, informasi mengenai API mana yang harus dibuat buffering protokol harus akan 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 aturan tertentu harus diterapkan. Rule -> fungsi Aspect.

Komplikasi yang agak tak terduga adalah bahwa aspek dapat melekat pada aspek lain; misalnya, aspek yang mengumpulkan classpath untuk Java IDE mungkin akan ingin tahu tentang semua file .jar di classpath, tetapi beberapa di antaranya {i>buffer<i} protokol. Dalam hal ini, aspek IDE perlu dilampirkan ke (aturan proto_library + aspek proto Java).

Kompleksitas aspek pada aspek dibahas di class AspectCollection.

Platform dan toolchain

Bazel mendukung build multi-platform, yaitu build yang mungkin beberapa arsitektur di mana tindakan build berjalan dan beberapa arsitektur untuk kode mana yang dibangun. Arsitektur ini disebut sebagai platform dalam Bazel bahasa (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" batasan yang paling umum digunakan setelan dan nilai dalam repositori @platforms.

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

Untuk melakukannya, toolchain dianotasi dengan set eksekusi dan menargetkan batasan platform yang mereka dukung. Untuk melakukannya, definisi dari toolchain dibagi menjadi dua bagian:

  1. Aturan toolchain() yang menjelaskan kumpulan 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 spesifik per bahasa yang menjelaskan toolchain aktual (seperti cc_toolchain())

Hal ini dilakukan dengan cara ini karena kita perlu mengetahui batasan untuk setiap toolchain untuk melakukan resolusi toolchain dan spesifik per bahasa Aturan *_toolchain() berisi lebih banyak informasi dari itu, sehingga dibutuhkan lebih banyak waktu pemuatan.

Platform eksekusi ditentukan dengan salah satu cara berikut:

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

Set platform eksekusi yang tersedia dikomputasi dalam RegisteredExecutionPlatformsFunction .

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

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

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

Hasilnya adalah UnloadedToolchainContext, yang pada dasarnya adalah peta dari jenis toolchain (direpresentasikan sebagai instance ToolchainTypeInfo) ke label toolchain yang dipilih. Ini disebut "dibongkar" karena tidak berisi toolchain 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 adanya satu "host" dan konfigurasi target yang diwakili oleh berbagai flag konfigurasi, seperti --cpu . Kami secara bertahap akan melakukan transisi ke sistem file. Untuk menangani kasus ketika pengguna mengandalkan konfigurasi lama kami, kami telah menerapkan pemetaan platform untuk menerjemahkan antara tanda lama dan batasan platform gaya baru. Kode mereka ada di PlatformMappingFunction dan menggunakan "little" non-Starlark "bahasa".

Batasan

Terkadang seseorang ingin menetapkan target sebagai hanya kompatibel dengan beberapa di seluruh platform Google. 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; mereka dalam perjalanan keluar dan itu tidak tersedia di Bazel, tetapi kode sumbernya mungkin berisi referensi yang merujuk padanya. 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 bisa dibangun, di mana "lingkungan" adalah instance dari aturan environment().

Ada berbagai cara menentukan lingkungan yang didukung untuk aturan:

  1. Melalui atribut restricted_to=. Ini adalah bentuk yang paling langsung spesifikasi; perintah ini mendeklarasikan kumpulan lingkungan yang tepat yang didukung aturan untuk grup ini.
  2. Melalui atribut compatible_with=. Perintah ini mendeklarasikan lingkungan sebagai aturan selain model "standar" yang didukung oleh secara default.
  3. Melalui atribut tingkat paket default_restricted_to= dan default_compatible_with=.
  4. Melalui spesifikasi default dalam aturan environment_group(). Setiap milik grup rekan yang secara tematik terkait (seperti "CPU arsitektur", "Versi JDK" atau "sistem operasi seluler"). Tujuan definisi grup lingkungan mencakup manakah dari harus didukung oleh "default" jika tidak ditentukan lain oleh Atribut restricted_to= / environment(). Aturan yang tidak memiliki akan mewarisi semua nilai default.
  5. Melalui default class aturan. Setelan ini akan menggantikan setelan default global untuk semua dari class aturan yang ditentukan. Ini dapat digunakan, misalnya, untuk membuat semua aturan *_test dapat diuji tanpa setiap instance harus secara eksplisit mendeklarasikan kemampuan ini.

environment() diterapkan sebagai aturan reguler sedangkan environment_group() baik subclass dari Target tetapi bukan Rule (EnvironmentGroup) dan yang tersedia secara {i>default<i} dari Starlark (StarlarkLibrary.environmentGroup()) yang pada akhirnya menciptakan eponim target. Hal ini untuk menghindari ketergantungan siklik yang akan muncul karena setiap lingkungan harus mendeklarasikan grup lingkungannya dan masing-masing grup lingkungan harus mendeklarasikan lingkungan defaultnya.

Sebuah build dapat dibatasi untuk lingkungan tertentu dengan Opsi command line --target_environment.

Implementasi pemeriksaan batasan sudah RuleContextConstraintSemantics dan TopLevelConstraintSemantics.

Batasan platform

"resmi" saat ini untuk mendeskripsikan platform apa yang kompatibel dengan target yaitu dengan menggunakan batasan yang sama dengan yang digunakan untuk menjelaskan toolchain dan platform. Dalam peninjauan di permintaan pull #10945.

Visibilitas

Jika Anda mengerjakan codebase besar dengan banyak developer (seperti di Google), ingin mencegah orang lain untuk secara acak bergantung pada pada kode sumber. Jika tidak, sesuai dengan hukum Hyrum, orang akan bergantung pada perilaku yang Anda anggap dapat diterapkan spesifikasi pendukung.

Bazel mendukung ini dengan mekanisme yang disebut visibilitas: Anda dapat mendeklarasikan bahwa target tertentu hanya dapat diandalkan pada penggunaan visibilitas. Ini sedikit khusus karena, meskipun memiliki daftar label, label dapat mengenkode pola melalui nama paket daripada pointer ke target tertentu. (Ya, ini adalah cacat desain.)

Perubahan ini diterapkan di tempat berikut:

  • Antarmuka RuleVisibility mewakili deklarasi visibilitas. Teknologi ini dapat berupa konstanta (sepenuhnya publik atau sepenuhnya pribadi) atau daftar label.
  • Label dapat merujuk ke salah satu grup paket (daftar paket yang ditentukan sebelumnya), untuk paket secara langsung (//pkg:__pkg__) atau subpohon paket (//pkg:__subpackages__). Ini berbeda dengan sintaks baris perintah, yang menggunakan //pkg:* atau //pkg/....
  • Grup paket diterapkan sebagai targetnya sendiri (PackageGroup) dan target yang dikonfigurasi (PackageGroupConfiguredTarget). Kita mungkin bisa gantilah dengan aturan sederhana jika kita mau. Logikanya diimplementasikan dengan bantuan: PackageSpecification, yang sesuai dengan pola tunggal seperti //pkg/...; PackageGroupContents, yang sesuai ke atribut packages package_group tunggal; dan PackageSpecificationProvider, yang merupakan gabungan melalui package_group dan includes-nya yang transitif.
  • Konversi dari daftar label visibilitas ke dependensi dilakukan di DependencyResolver.visitTargetVisibility dan beberapa jenis lainnya tempat.
  • Pemeriksaan yang sebenarnya dilakukan di CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()

Kumpulan bertingkat

Sering kali, target yang dikonfigurasi menggabungkan serangkaian file dari dependensinya, menambahkan dirinya sendiri, dan menggabungkan kumpulan agregat ke 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 harus ada di classpath untuk aturan Java untuk mengompilasi atau menjalankan
  • Kumpulan file Python dalam penutupan transitif aturan Python

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

Untuk mengatasi masalah ini, kami memiliki konsep sebuah NestedSet. Ini adalah struktur data yang terdiri dari NestedSet lainnya dan beberapa anggotanya sendiri, sehingga membentuk grafik asiklik terarah dari {i>dataset<i}. ID tersebut tidak dapat diubah dan anggotanya dapat diiterasi. Kami mendefinisikan beberapa urutan iterasi (NestedSet.Order): praorder, postorder, topologi ({i>node<i} selalu muncul setelah ancestor-nya) dan "tidak peduli, tetapi seharusnya sama setiap kali".

Struktur data yang sama disebut depset di Starlark.

Artefak dan Tindakan

Versi sebenarnya terdiri dari serangkaian perintah yang perlu dijalankan untuk menghasilkan {i>output<i} yang diinginkan pengguna. Perintah-perintah tersebut dinyatakan sebagai instance class Action dan file direpresentasikan sebagai instance class Artifact. Mereka tersusun dalam grafik bipartit, terarah, asiklik yang disebut "grafik tindakan".

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

  1. **Artefak reguler. **Data ini diperiksa untuk mengetahui informasi terbaru dengan menghitung {i>checksum<i} mereka, dengan {i>mtime<i} sebagai pintasan; kita tidak melakukan {i>checksum<i} pada file jika {i>ctime<i} belum berubah.
  2. Artefak symlink yang belum di-resolve. Dokumen ini diperiksa untuk mengetahui memanggil readlink(). Tidak seperti artefak biasa, ini bisa menggantung {i>symlink<i}. Biasanya digunakan dalam kasus di mana satu file kemudian mengemas beberapa file ke dalam semacam arsip.
  3. Artefak pohon. Ini bukan file tunggal, melainkan pohon direktori. Mereka diperiksa {i>up-to-date<i} dengan cara memeriksa kumpulan file di dalamnya dan konten. Class tersebut direpresentasikan sebagai TreeArtifact.
  4. Artefak metadata konstan. Perubahan pada artefak ini tidak memicu membangun kembali. Ini digunakan secara eksklusif untuk informasi cap build: kita tidak ingin melakukan {i>rebuild<i} 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 seharusnya -- merujuk ke direktori sumber dalam file BUILD adalah salah satu beberapa masalah kesalahan yang sudah lama ada pada Bazel; kita punya pekerjaan semacam itu, yang dimungkinkan oleh properti JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1)

Jenis Artifact yang terkenal adalah perantara. Hal ini ditunjukkan dengan Artifact instance yang merupakan output dari MiddlemanAction. {i>Mockup <i}digunakan untuk beberapa hal khusus:

  • Agregat perantara digunakan untuk mengelompokkan artefak bersama-sama. Ini agar jika banyak tindakan menggunakan set input besar yang sama, kita tidak memiliki N*M tepi dependensi, hanya N+M (mereka diganti dengan kumpulan bertingkat)
  • Penjadwalan perantara dependensi memastikan bahwa tindakan berjalan sebelum tindakan lainnya. Skrip ini sebagian besar digunakan untuk analisis lint tetapi juga untuk kompilasi C++ (lihat CcCompilationContext.createMiddleman() untuk penjelasan)
  • Perantara Runfile digunakan untuk memastikan adanya hierarki {i>runfile<i} sehingga yang tidak secara terpisah perlu bergantung pada manifes {i>output<i} dan setiap satu artefak yang dirujuk oleh hierarki runfiles.

Tindakan paling baik dipahami sebagai perintah yang perlu dijalankan, yaitu lingkungan yang dibutuhkannya dan serangkaian {i> output<i} yang dihasilkannya. Hal-hal berikut adalah hal utama komponen deskripsi suatu tindakan:

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

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

Kami akhirnya ingin memindahkan semuanya ke SpawnAction; JavaCompileAction sama dengan cukup mendekati, tetapi C++ merupakan kasus khusus karena penguraian file {i> .d<i} dan termasuk pemindaian.

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

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

Tindakan yang dibagikan

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

Hal ini dianggap sebagai suatu kesalahan fitur, tetapi menyingkirkannya sangat sulit karena menghasilkan penghematan waktu eksekusi yang signifikan saat, misalnya, file sumber perlu diproses entah bagaimana dan file itu dirujuk oleh beberapa aturan (gelombang tangan). Hal ini membutuhkan beberapa RAM: masing-masing RAM {i>instance<i} dari tindakan bersama perlu disimpan dalam memori secara terpisah.

Jika dua tindakan menghasilkan file output yang sama, keduanya harus sama persis: memiliki input, {i>output<i} yang sama, dan menjalankan baris perintah yang sama. Ini kesetaraan diimplementasikan di Actions.canBeShared() dan diverifikasi antara fase analisis dan eksekusi dengan melihat setiap Tindakan. Hal ini diterapkan di SkyframeActionExecutor.findAndStoreArtifactConflicts() dan merupakan salah satu dari beberapa tempat di Bazel yang memerlukan tampilan buat.

Fase eksekusi

Pada saat inilah Bazel benar-benar mulai menjalankan tindakan {i>build<i}, seperti perintah yang menghasilkan {i>output<i}.

Hal pertama yang dilakukan Bazel setelah fase analisis adalah menentukan apa Artefak perlu dibangun. Logika untuk ini dikodekan dalam TopLevelArtifactHelper; secara garis besar, itu adalah filesToBuild dari target yang dikonfigurasi pada baris perintah dan konten {i>output<i} khusus dengan tujuan eksplisit untuk menyatakan "jika target ini berada pada perintah ini, bangun 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), aplikasi harus menyediakan tindakan yang dieksekusi secara lokal dengan hierarki sumber lengkap. Ini adalah ditangani oleh class SymlinkForest dan bekerja dengan mencatat setiap target digunakan dalam fase analisis dan membangun hierarki direktori tunggal yang menghubungkan setiap paket dengan target yang digunakan dari lokasi sebenarnya. Alternatifnya akan adalah meneruskan jalur yang benar ke perintah (dengan mempertimbangkan --package_path). Hal ini tidak diinginkan karena:

  • Mengubah command line tindakan saat paket dipindahkan dari jalur paket entri ke akun lain (sebelumnya umum terjadi)
  • Ini menghasilkan baris perintah yang berbeda jika tindakan dijalankan dari jarak jauh berjalan secara lokal,
  • 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 aksi (grafik terarah bipartit yang terdiri dari tindakan dan artefak input dan outputnya) dan menjalankan tindakan. Eksekusi setiap tindakan direpresentasikan oleh instance SkyValue kelas ActionExecutionValue.

Karena menjalankan tindakan itu mahal, kita memiliki beberapa lapisan {i>caching<i} yang dapat terkena serangan di belakang Skyframe:

  • ActionExecutionFunction.stateMap berisi data untuk membuat Skyframe dimulai ulang dari ActionExecutionFunction murah
  • Cache tindakan lokal berisi data tentang status sistem file
  • Sistem eksekusi jarak jauh biasanya juga berisi cache-nya sendiri

Cache tindakan lokal

{i>Cache<i} ini adalah lapisan lain yang berada di belakang Skyframe; bahkan jika suatu tindakan dijalankan kembali di Skyframe, masih bisa menjadi hit di cache tindakan lokal. Ini mewakili keadaan sistem file lokal dan diserialisasi ke {i>disk<i} yang berarti bahwa ketika seseorang menjalankan server Bazel baru, seseorang bisa mendapatkan {i>cache<i} tindakan lokal jika 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. Tindakannya dijelaskan sebagai:

  1. Kumpulan file {i>input<i} dan {i>output<i} dan {i>checksum<i}-nya
  2. "Kunci aksinya", yang biasanya merupakan baris perintah yang dijalankan, tetapi secara umum, mewakili segala sesuatu yang tidak diambil oleh {i>checksum <i}dari file input (seperti untuk FileWriteAction, ini adalah checksum data yang tertulis)

Ada juga "cache tindakan top-down" yang sangat eksperimental yang masih dalam pengembangan, yang menggunakan {i>hash <i} transitif untuk menghindari masuk ke {i>cache<i} sebanyak kali.

Penemuan input dan pemangkasan input

Beberapa tindakan lebih rumit daripada sekadar memiliki satu set input. Perubahan pada satu set input dari suatu tindakan datang dalam dua bentuk:

  • Suatu tindakan dapat menemukan {i>input<i} baru sebelum dieksekusi atau memutuskan bahwa beberapa inputnya sebenarnya tidak diperlukan. Contoh kanonis adalah C++, di mana lebih baik membuat perkiraan yang matang tentang file {i>header<i} apa yang digunakan oleh file dari penutupan transitifnya sehingga kita tidak memperhatikan setiap pengiriman ke eksekutor jarak jauh; Oleh karena itu, kita memiliki pilihan untuk tidak mendaftarkan setiap sebagai "input", tetapi memindai file sumber secara transitif header yang disertakan dan hanya menandai file header tersebut sebagai input yang yang disebutkan dalam pernyataan #include (kami memberikan perkiraan yang lebih tinggi sehingga kami tidak perlu mengimplementasikan praprosesor C penuh) Opsi ini saat ini dihubungkan dengan kabel untuk "salah" di Bazel dan hanya digunakan di Google.
  • Suatu tindakan mungkin menyadari bahwa beberapa file tidak digunakan selama eksekusinya. Di beberapa C++, ini disebut "file .d": compiler memberi tahu file {i>header<i} mana yang digunakan sesuai fakta, dan untuk menghindari rasa malu karena inkrementalitas daripada Make, Bazel memanfaatkan fakta ini. Hal ini menawarkan dibandingkan dengan pemindai {i>include<i} karena mengandalkan kompiler.

Hal ini diimplementasikan menggunakan metode pada Tindakan:

  1. Action.discoverInputs() dipanggil. Ini akan mengembalikan serangkaian perangkat yang bersarang Artefak yang dianggap wajib. Ini harus berupa artefak sumber sehingga tidak ada tepi dependensi di grafik tindakan yang tidak memiliki 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 mengakibatkan build inkremental yang salah jika input yang digunakan dilaporkan sebagai tidak terpakai.

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

Tindakan Starlark dapat memanfaatkan 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, baris perintah dapat dijalankan secara lokal, tetapi dalam berbagai jenis sandbox, atau dari jarak jauh. Tujuan konsep yang mewujudkannya disebut ActionContext (atau Strategy, karena kita berhasil berjalan hanya setengah jalan dengan penggantian nama...)

Siklus hidup konteks tindakan adalah sebagai berikut:

  1. Saat fase eksekusi dimulai, instance BlazeModule ditanyai apa konteks tindakan yang mereka miliki. Ini terjadi di konstruktor ExecutionTool. Jenis konteks tindakan diidentifikasi oleh Class Java yang merujuk pada sub-antarmuka dari ActionContext dan yang harus diimplementasikan oleh konteks tindakan.
  2. Konteks tindakan yang tepat dipilih dari yang tersedia dan diteruskan ke ActionExecutionContext dan BlazeExecutor .
  3. Tindakan meminta konteks menggunakan ActionExecutionContext.getContext() dan BlazeExecutor.getStrategy() (seharusnya hanya ada satu cara untuk melakukan itu...)

Strategi bebas menyebutkan strategi lain dalam melakukan pekerjaan mereka; ini digunakan, untuk misalnya, dalam strategi dinamis yang memulai tindakan baik secara lokal maupun jarak jauh, kemudian menggunakan opsi yang selesai lebih dahulu.

Salah satu strategi terkenal adalah strategi yang mengimplementasikan proses pekerja yang persisten (WorkerSpawnStrategy). Idenya adalah, beberapa alat memiliki waktu {i>startup<i} yang lama dan karenanya harus digunakan kembali antartindakan alih-alih memulai yang baru untuk setiap tindakan (Ini merupakan potensi masalah yang benar, karena Bazel mengandalkan janji proses pekerja yang tidak membawa data yang dapat diamati status di antara permintaan individual)

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

Informasi selengkapnya tentang strategi (atau konteks tindakan!):

  • Tersedia informasi tentang berbagai strategi untuk menjalankan tindakan di sini.
  • Informasi tentang strategi dinamis, tempat kami menjalankan tindakan secara lokal dan jarak jauh untuk melihat penyelesaian mana yang lebih dulu 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 akan berbeda dari satu tindakan ke tindakan lainnya: makin banyak resource tindakan diperlukan, semakin sedikit {i>instance <i} yang harus berjalan pada saat yang sama untuk menghindari membebani komputer lokal.

Hal ini diterapkan di class ResourceManager: setiap tindakan harus dianotasi dengan perkiraan sumber daya lokal yang diperlukan dalam bentuk Instance ResourceSet (CPU dan RAM). Kemudian, ketika konteks tindakan melakukan sesuatu yang memerlukan resource lokal, mereka memanggil ResourceManager.acquireResources() dan diblokir sampai sumber daya yang diperlukan tersedia.

Deskripsi lebih detail tentang pengelolaan sumber daya lokal tersedia di sini.

Struktur direktori output

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

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

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

  1. Jika dua konfigurasi bisa muncul di build yang sama, keduanya harus memiliki direktori yang berbeda sehingga keduanya dapat memiliki versi mereka sendiri yang sama tindakan; jika tidak sepakat mengenai perbedaan pendapat, dari aksi yang menghasilkan file {i>output<i} yang sama, Bazel tidak tahu mana tindakan yang harus dipilih ("konflik tindakan")
  2. Jika dua konfigurasi mewakili "kira-kira" hal yang sama, mereka harus memiliki nama yang sama sehingga tindakan yang dieksekusi di satu dapat digunakan kembali jika baris perintah cocok: misalnya, perubahan opsi baris perintah untuk compiler Java seharusnya tidak mengakibatkan tindakan kompilasi C++ dijalankan ulang.

Sejauh ini, kami belum menemukan cara pasti untuk memecahkan masalah ini, yang memiliki kesamaan dengan masalah {i>trimming<i} konfigurasi. Diskusi yang lebih panjang opsi tersedia di sini. Area bermasalah utama adalah aturan Starlark (yang penulisnya biasanya sangat akrab dengan Bazel) dan aspek-aspek, yang menambahkan dimensi lain pada ruang lingkup yang dapat menghasilkan hal yang sama file output.

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

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 pengumpulan waktu tertentu)
  • 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 bagaimana pengujian harus dijalankan:

  • Artefak yang menghasilkan build dalam pengujian sedang dijalankan. Ini adalah "cache status" file 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. Tujuan ekspansi diterapkan di TestsForTargetPatternFunction. Agak peduli yang mengejutkan adalah jika {i> test suite<i} mendeklarasikan tidak ada pengujian, itu mengacu pada setiap pengujian dalam paketnya. Hal ini diterapkan 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 hasil dimasukkan ke dalam TargetPatternPhaseValue.getTestsToRunLabels(). Alasannya mengapa atribut aturan yang dapat difilter tidak dapat dikonfigurasi adalah bahwa terjadi sebelum fase analisis, oleh karena itu, konfigurasi tidak yang tersedia.

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

Untuk memberikan transparansi pada proses yang rumit ini, tests() operator kueri (diterapkan di TestsFunction) tersedia untuk mengetahui pengujian mana dijalankan ketika target tertentu ditetapkan pada baris perintah. Penting sayangnya implementasi ulang, jadi mungkin menyimpang dari yang disebutkan di atas dengan berbagai cara.

Menjalankan pengujian

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

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

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

  • test.xml, file XML gaya JUnit yang menjelaskan setiap kasus pengujian di shard pengujian
  • test.log, output konsol pengujian. {i>stdout<i} dan {i>stderr<i} tidak secara terpisah.
  • test.outputs, "direktori output yang tidak dideklarasikan"; ini digunakan dalam pengujian yang ingin menghasilkan {i>output<i} file selain apa yang mereka cetak ke terminal.

Ada dua hal yang bisa terjadi selama pelaksanaan uji coba membangun 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 diperoleh dengan menambahkan tags=["exclusive"] ke aturan pengujian atau menjalankan pengujian dengan --test_strategy=exclusive . Setiap penawaran eksklusif dijalankan dengan pemanggilan Skyframe terpisah yang meminta eksekusi uji setelah "utama" buat. 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 kemajuan pengujian yang berjalan lama. Hal ini ditentukan oleh Opsi command line --test_output=streamed dan menyiratkan pengujian eksklusif sehingga {i>output<i} dari pengujian yang berbeda tidak diselingi.

Hal ini diimplementasikan di class StreamedTestOutput yang diberi nama dengan tepat dan digunakan dengan polling perubahan pada file test.log dari pengujian yang dimaksud dan membuang {i>byte<i} ke terminal tempat Bazel beraturan.

Hasil pengujian yang dilakukan tersedia di bus peristiwa dengan mengamati berbagai peristiwa (seperti TestAttempt, TestResult, atau TestingCompleteEvent). Peristiwa tersebut dibuang ke Build Event Protocol dan dikirim ke konsol paling lambat 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 di mana file cakupan ditulis oleh runtime cakupan. Kemudian sistem menjalankan pengujian. Sebuah pengujian dapat menjalankan beberapa subproses dan terdiri dari bagian yang ditulis dalam berbagai bahasa pemrograman yang berbeda (dengan bagian runtime kumpulan cakupan). Skrip wrapper bertanggung jawab untuk mengonversi file yang dihasilkan ke format LCOV jika perlu, dan menggabungkannya menjadi satu .

Interposisi collect_coverage.sh dilakukan oleh strategi pengujian dan mengharuskan collect_coverage.sh untuk berada di input pengujian. Ini adalah dicapai dengan atribut implisit :coverage_support yang diselesaikan menjadi nilai flag konfigurasi --coverage_support (lihat TestConfiguration.TestOptions.coverageSupport)

Beberapa bahasa melakukan instrumentasi offline, artinya cakupan instrumentasi ditambahkan pada waktu kompilasi (seperti C++) dan yang lainnya melakukannya secara online instrumentasi, yang berarti instrumentasi cakupan ditambahkan saat eksekusi baik.

Konsep inti lainnya adalah cakupan dasar pengukuran. Ini adalah cakupan dari sebuah perpustakaan, biner, atau menguji jika tidak ada kode di dalamnya yang dijalankan. Masalah yang dipecahkan adalah jika Anda ingin menghitung cakupan pengujian untuk biner, tidak cukup untuk menggabungkan cakupan semua tes karena mungkin ada kode dalam biner yang tidak ditautkan ke pengujian apa pun. Oleh karena itu, yang kita lakukan adalah memancarkan file cakupan untuk setiap biner yang hanya berisi file yang cakupannya kami kumpulkan tanpa ditanggung penting. File cakupan dasar pengukuran untuk target berada pada bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat . ID ini juga dibuat untuk biner dan pustaka selain untuk pengujian jika Anda meneruskan --nobuild_tests_only untuk Bazel.

Cakupan dasar saat ini rusak.

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

Hanya itu kumpulan file berinstrumen, yaitu kumpulan file untuk diinstrumentasikan. Sebagai runtime cakupan online, ini dapat digunakan pada waktu {i>runtime<i} untuk memutuskan file mana instrumentasi. Cakupan juga digunakan untuk menerapkan cakupan dasar.

Kumpulan file metadata instrumentasi adalah kumpulan file tambahan yang diperlukan pengujian untuk menghasilkan file LCOV yang dibutuhkan 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 mengaktifkan pembuatan versi.

Apakah cakupan sedang dikumpulkan atau tidak disimpan dalam BuildConfiguration. Ini berguna karena merupakan cara mudah untuk mengubah pengujian aksi dan grafik aksi tergantung pada bit ini, tetapi itu juga berarti bahwa jika sedikit dibalik, semua target harus dianalisis ulang (beberapa bahasa, seperti C++ memerlukan opsi compiler yang berbeda untuk memancarkan kode yang dapat mengumpulkan, yang agak mengurangi masalah ini, karena analisis ulang tetap diperlukan).

File dukungan cakupan bergantung pada melalui label secara implisit dependensi sehingga dapat diganti oleh kebijakan pemanggilan, yang memungkinkan berbeda di antara versi Bazel yang berbeda. Idealnya, semua perbedaan akan dihilangkan, dan kami standarisasi pada salah satunya.

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

Mesin kueri

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

  • 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 kami. Untuk mengizinkan hasil kueri streaming, alih-alih mengumpulkannya ke beberapa struktur data, query2.engine.Callback diteruskan ke QueryFunction, yang memanggilnya untuk hasil yang ingin dikembalikan.

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

Persyaratan halus dari beberapa format {i>output <i} kueri (tentunya proto) adalah Bazel perlu memunculkan _semua _informasi yang disediakan oleh pemuatan paket agar kita dapat melakukan diff pada {i>output<i} dan menentukan apakah target tertentu telah berubah. Akibatnya, nilai atribut harus dapat diserialisasi, itulah sebabnya hanya sedikit jenis atribut tanpa atribut yang memiliki Starlark yang kompleks masing-masing. Solusi yang biasa adalah menggunakan label, dan melampirkan kompleksitas informasi ke aturan dengan label tersebut. Ini bukan solusi yang sangat memuaskan dan sebaiknya Anda mencabut persyaratan ini.

Sistem modul

Bazel dapat diperluas dengan menambahkan modul ke dalamnya. Setiap modul harus dijadikan subclass BlazeModule (nama ini adalah relik sejarah Bazel yang dulunya yang disebut Blaze) dan mendapatkan informasi tentang berbagai peristiwa selama eksekusi perintah.

Mereka sebagian besar digunakan untuk mengimplementasikan berbagai bagian fungsionalitas bahwa hanya beberapa versi Bazel (seperti yang kami gunakan di Google) yang memerlukan:

  • Antarmuka ke sistem eksekusi jarak jauh
  • Perintah baru

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

{i>Bus<i} acara

Cara utama BlazeModules berkomunikasi dengan anggota Bazel lainnya adalah dengan bus acara (EventBus): instance baru dibuat untuk setiap build, berbagai bagian Bazel bisa memposting peristiwa ke sana dan modul bisa mendaftarkan pemroses untuk peristiwa membuat Anda tertarik. Misalnya, hal-hal berikut ditampilkan sebagai peristiwa:

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

Beberapa dari acara ini ditampilkan di luar Bazel dalam Protokol Peristiwa Build (yakni BuildEvent). Ini tidak hanya memungkinkan BlazeModule, tetapi juga berbagai hal di luar proses Bazel untuk mengamati build. Dokumen tersebut dapat diakses sebagai file yang berisi pesan protokol atau Bazel dapat terhubung ke server (disebut {i>Build Event Service<i}) untuk mengalirkan peristiwa.

Hal ini diterapkan di build.lib.buildeventservice dan build.lib.buildeventstream paket Java.

Repositori eksternal

Sedangkan Bazel pada awalnya dirancang untuk digunakan dalam monorepo (satu sumber pohon yang berisi segala sesuatu yang dibutuhkan untuk membangun), Bazel tinggal di dunia di mana ini belum tentu benar. "Repositori eksternal" merupakan abstraksi yang digunakan untuk menjembatani dua dunia ini: mereka mewakili kode yang diperlukan untuk build tetapi tidak ada 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. Dari mana rumit adalah Anda dapat menentukan aturan repositori baru dalam file Starlark, dapat digunakan untuk memuat kode Starlark baru, yang dapat digunakan untuk menentukan aturan repositori dan sebagainya...

Untuk menangani kasus ini, penguraian file WORKSPACE (di WorkspaceFileFunction) dipecah menjadi beberapa bagian yang dipisahkan oleh load() pernyataan pribadi Anda. Indeks potongan ditunjukkan oleh WorkspaceFileKey.getIndex() dan menghitung WorkspaceFileFunction hingga indeks X berarti mengevaluasinya sampai Pernyataan load() ke-X.

Mengambil repositori

Sebelum kode repositori tersedia untuk Bazel, kode itu perlu diambil. Hal ini menyebabkan Bazel membuat direktori di bawah $OUTPUT_BASE/external/<repository name>.

Mengambil repositori dilakukan dalam langkah-langkah berikut:

  1. PackageLookupFunction menyadari bahwa ia memerlukan repositori dan membuat RepositoryName sebagai SkyKey, yang memanggil RepositoryLoaderFunction
  2. RepositoryLoaderFunction meneruskan permintaan ke RepositoryDelegatorFunction karena alasan yang tidak jelas (kode ini menyatakan bahwa menghindari mengunduh ulang sesuatu jika Skyframe dimulai ulang, tapi ini bukan alasan yang sangat kuat)
  3. RepositoryDelegatorFunction menemukan aturan repositori yang diminta ambil dengan melakukan iterasi pada potongan file WORKSPACE sampai repositori ditemukan
  4. RepositoryFunction yang sesuai ditemukan yang mengimplementasikan repositori fetching; bisa berupa implementasi Starlark dari repositori atau peta hard code untuk repositori yang diimplementasikan di Java.

Ada berbagai lapisan {i>caching<i} karena mengambil repositori bisa sangat mahal:

  1. Ada {i>cache<i} untuk file yang diunduh yang terkunci oleh {i>checksum<i}-nya (RepositoryCache). Hal ini mengharuskan {i>checksum<i} tersedia di WORKSPACE, tapi itu tetap bagus untuk Hermeticity. Ini dibagikan oleh setiap {i>instance<i} server Bazel pada {i> workstation<i} yang sama, terlepas dari atau basis output tempat mereka berjalan.
  2. "File penanda" ditulis untuk setiap repositori di $OUTPUT_BASE/external yang berisi {i>checksum<i} dari aturan yang digunakan untuk mengambilnya. Jika Bazel server dimulai ulang tetapi {i>checksum<i} tidak berubah, tidak diambil kembali. Ini diterapkan di RepositoryDelegatorFunction.DigestWriter .
  3. Opsi command line --distdir menetapkan cache lain yang digunakan untuk mencari artefak yang akan diunduh. Hal ini berguna dalam setelan perusahaan di mana Bazel tidak boleh mengambil hal acak dari Internet. Ini adalah yang diimplementasikan oleh DownloadManager .

Setelah repositori didownload, artefak di dalamnya diperlakukan sebagai sumber artefak. Hal ini menimbulkan masalah karena Bazel biasanya memeriksa artefak sumber dengan memanggil {i>stat()<i}, dan artefak ini juga menjadi tidak valid ketika definisi repositori yang sedang berubah menjadi tidak valid. Dengan demikian, FileStateValue untuk artefak dalam repositori eksternal yang perlu bergantung pada repositori eksternal mereka. Hal ini ditangani oleh ExternalFilesHelper.

Pemetaan repositori

Bisa jadi beberapa repositori ingin bergantung pada repositori yang sama, tetapi dalam versi yang berbeda (ini adalah instance "dependensi berlian tertentu"). Misalnya, jika dua biner dalam repositori terpisah dalam build ingin bergantung pada Guava, mereka mungkin akan merujuk ke Guava dengan label mulai @guava// dan memperkirakannya akan memiliki versi yang berbeda.

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

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

Pemetaan ditetapkan di file WORKSPACE sebagai atribut repo_mapping definisi repositori individual. Kemudian muncul di Skyframe sebagai anggota WorkspaceFileValue, yang ditautkan ke:

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

Bit JNI

Server Bazel sebagian besar ditulis dalam Java. Pengecualiannya adalah bagian-bagian yang Java tidak bisa melakukannya sendiri atau tidak bisa melakukannya sendiri ketika kita mengimplementasikannya. 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 tersebut adalah:

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

Output konsol

Memancarkan {i>output<i} konsol tampak seperti hal yang sederhana, tetapi pertemuan banyak proses (kadang-kadang dari jarak jauh), {i>caching<i} yang sangat terperinci, keinginan untuk memiliki {i>output<i} terminal yang bagus dan penuh warna dan memiliki server yang berjalan lama membuat maka hal ini tidaklah sepele.

Tepat setelah panggilan RPC masuk dari klien, dua RpcOutputStream dibuat (untuk {i>stdout<i} dan {i>stderr<i} yang meneruskan data yang dicetak ke dalam mereka kepada klien. Objek ini kemudian digabungkan dalam OutErr ((stdout, stderr) tertentu). Apa pun yang perlu dicetak di konsol akan melalui proses feed. Kemudian aliran data ini diserahkan kepada BlazeCommandDispatcher.execExclusively().

Output secara default dicetak dengan urutan escape ANSI. Bila ini bukan diinginkan (--color=no), dihapus oleh AnsiStrippingOutputStream. Di beberapa Selain itu, System.out dan System.err akan dialihkan ke aliran output ini. Ini dimaksudkan agar informasi {i>debugging<i} dapat dicetak menggunakan System.err.println() dan masih berakhir di output terminal klien (yang berbeda dari server). Perlu diperhatikan bahwa jika sebuah proses menghasilkan output biner (seperti bazel query --output=proto), tanpa munging stdout berlangsung.

Pesan singkat (error, peringatan, dan sejenisnya) dinyatakan melalui Antarmuka EventHandler. Secara khusus, ini berbeda dari postingan EventBus (ini membingungkan). Setiap Event memiliki EventKind (error, peringatan, info, dan beberapa lainnya) dan mereka mungkin memiliki Location (tempat di kode sumber yang menyebabkan peristiwa terjadi).

Beberapa implementasi EventHandler menyimpan peristiwa yang diterimanya. Ini digunakan memutar ulang informasi ke UI yang disebabkan oleh berbagai jenis pemrosesan {i>cache<i}, misalnya, peringatan yang dikeluarkan oleh target yang dikonfigurasi dalam cache.

Beberapa EventHandler juga mengizinkan postingan acara yang pada akhirnya akan ditemukan bus peristiwa (Event reguler _not _muncul di sana). Berikut adalah implementasi ExtendedEventHandler dan penggunaan utamanya adalah untuk memutar ulang cache EventBus peristiwa. Semua peristiwa EventBus ini menerapkan Postable, tetapi tidak semua yang diposting ke EventBus harus mengimplementasikan antarmuka ini; hanya yang di-cache oleh ExtendedEventHandler (akan lebih baik dan yang biasanya dilakukan; tapi tidak diterapkan)

Output terminal sebagian besar dipancarkan melalui UiEventHandler, yang bertanggung jawab atas semua pemformatan {i>output<i} dan pelaporan kemajuan yang canggih, fungsi tersebut. Kode ini memiliki dua input:

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

Satu-satunya koneksi langsung ke mesin eksekusi perintah (misalnya Bazel) harus melakukan streaming RPC ke klien melalui Reporter.getOutErr(), yang memungkinkan akses langsung ke aliran data ini. Ini hanya digunakan ketika sebuah perintah membutuhkan untuk 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 batas kemampuan AI 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 dengan tepat diberi nama Profiler. Ini diaktifkan secara default, meskipun hanya merekam data yang diringkas sehingga {i> overhead<i}-nya dapat ditoleransi; Command line --record_full_profiler_data membuatnya merekam semua hal yang bisa dilakukannya.

Alat ini memunculkan profil dalam format profiler Chrome; paling baik dilihat di Chrome. Model datanya adalah tumpukan tugas: seseorang dapat memulai tugas dan mengakhiri tugas dan mereka seharusnya bersarang dengan rapi satu sama lain. Setiap thread Java mendapatkan tumpukan tugasnya sendiri. TODO: Bagaimana cara kerjanya dengan tindakan dan gaya {i>continuation-passing<i}?

Profiler dimulai dan dihentikan di BlazeRuntime.initProfiler() dan BlazeRuntime.afterCommand() masing-masing dan berupaya untuk aktif selama mungkin sehingga kita dapat membuat profil semuanya. Untuk menambahkan sesuatu ke profil, panggil Profiler.instance().profile(). Metode ini menampilkan Closeable, yang penutupannya mewakili akhir tugas. Opsi ini paling baik digunakan dengan try-with-resources pernyataan pribadi Anda.

Kita juga melakukan pembuatan profil memori dasar di MemoryProfiler. Fitur ini juga selalu aktif dan kebanyakan mencatat ukuran heap maksimum dan perilaku GC.

Menguji Bazel

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

Pengujian integrasi memiliki dua jenis:

  1. Yang diimplementasikan menggunakan kerangka kerja pengujian bash yang sangat rumit src/test/shell
  2. One diimplementasikan di Java. Ini diimplementasikan sebagai subclass BuildIntegrationTestCase

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

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