Dokumen ini adalah deskripsi codebase dan cara Bazel disusun. Alat ini ditujukan untuk orang yang bersedia berkontribusi pada Bazel, bukan untuk pengguna akhir.
Pengantar
Basis kode Bazel sangat besar (~350KLOC kode produksi dan ~260 KLOC kode pengujian) dan tidak ada yang memahami seluruh lanskap: semua orang mengetahui lembah tertentu dengan sangat baik, tetapi hanya sedikit yang mengetahui apa yang ada di atas bukit di setiap arah.
Agar orang-orang yang berada di tengah perjalanan tidak merasa berada di dalam hutan yang gelap dengan jalur yang mudah hilang, dokumen ini mencoba memberikan ringkasan tentang code base sehingga lebih mudah untuk memulai mengerjakannya.
Versi publik kode sumber Bazel tersedia di GitHub di github.com/bazelbuild/bazel. Ini bukan "sumber tepercaya"; 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 tepercaya.
Kontribusi diterima melalui mekanisme permintaan pull GitHub reguler, dan diimpor secara manual oleh Googler ke hierarki sumber internal, lalu diekspor kembali ke GitHub.
Arsitektur klien/server
Sebagian besar Bazel berada dalam proses server yang tetap berada di RAM di antara build. Hal ini memungkinkan Bazel mempertahankan status di antara build.
Inilah sebabnya command line Bazel memiliki dua jenis opsi: startup dan perintah. Di command line seperti ini:
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
Beberapa opsi (--host_jvm_args=
) berada sebelum nama perintah yang akan dijalankan
dan beberapa berada setelahnya (-c opt
); jenis pertama disebut "opsi startup" dan
memengaruhi proses server secara keseluruhan, sedangkan jenis kedua, "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 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 yang diterapkan di C++ (
"klien") akan mendapatkan kontrol. Tindakan ini menyiapkan proses server yang sesuai menggunakan
langkah-langkah berikut:
- Memeriksa apakah file tersebut telah diekstrak. Jika tidak, aplikasi akan melakukannya. Di sinilah implementasi server berasal.
- Memeriksa apakah ada instance server aktif yang berfungsi: berjalan,
memiliki opsi startup yang tepat, dan menggunakan direktori ruang kerja yang tepat. Tindakan
ini menemukan server yang berjalan dengan melihat direktori
$OUTPUT_BASE/server
yang terdapat file kunci dengan port yang didengarkan oleh server. - Jika perlu, menghentikan proses server lama
- Jika diperlukan, mulai proses server baru
Setelah proses server yang sesuai siap, perintah yang perlu dijalankan
akan dikomunikasikan melalui antarmuka gRPC, lalu output Bazel akan disalurkan kembali
ke terminal. Hanya satu perintah yang dapat berjalan 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
agak memalukan. Pemblokir utama adalah siklus proses BlazeModule
dan beberapa status di BlazeRuntime
.
Di akhir perintah, server Bazel mengirimkan kode keluar yang harus ditampilkan
klien. Hal 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.
Saat pengguna menekan Ctrl-C, klien akan menerjemahkannya ke panggilan Cancel pada koneksi gRPC, yang mencoba menghentikan perintah sesegera mungkin. Setelah Ctrl-C ketiga, klien akan mengirim SIGKILL ke server.
Kode sumber klien berada di 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 kumpulan direktori yang agak rumit selama build. Deskripsi lengkap tersedia di Tata letak direktori output.
"workspace" adalah hierarki sumber tempat Bazel dijalankan. Biasanya, ini sesuai dengan sesuatu yang Anda check out dari kontrol sumber.
Bazel menempatkan semua datanya di "root pengguna output". 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 di bawah
basis penginstalan. Semuanya berada di $OUTPUT_USER_ROOT/install
secara default dan dapat diubah
menggunakan opsi command line --install_base
.
"Output base" adalah tempat instance Bazel yang dilampirkan ke ruang kerja
tertentu menulis. 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>
. Hal ini dapat diubah menggunakan opsi startup --output_base
,
yang, antara lain, berguna untuk mengatasi batasan bahwa hanya
satu instance Bazel yang dapat berjalan di ruang kerja mana pun pada waktu tertentu.
Direktori output berisi, antara lain:
- Repositori eksternal yang diambil di
$OUTPUT_BASE/external
. - Root exec, direktori yang berisi symlink ke semua kode
sumber untuk build saat ini. Berlokasi di
$OUTPUT_BASE/execroot
. Selama build, direktori kerja 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 di-build selama build.
Proses menjalankan perintah
Setelah server Bazel mendapatkan kontrol dan diberi tahu tentang perintah yang perlu dijalankan, urutan peristiwa berikut akan terjadi:
BlazeCommandDispatcher
diberi tahu tentang permintaan baru. Fungsi ini 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.Perintah yang tepat ditemukan. Setiap perintah harus mengimplementasikan antarmuka
BlazeCommand
dan harus memiliki anotasi@Command
(ini sedikit antipola, sebaiknya semua metadata yang diperlukan perintah dijelaskan oleh metode diBlazeCommand
)Opsi command line diuraikan. Setiap perintah memiliki opsi command line yang berbeda, yang dijelaskan dalam anotasi
@Command
.Bus peristiwa dibuat. Bus peristiwa adalah aliran data untuk peristiwa yang terjadi selama build. Beberapa di antaranya diekspor ke luar Bazel di bawah naungan Build Event Protocol untuk memberi tahu dunia tentang proses build yang sedang berlangsung.
Perintah tersebut mendapatkan kontrol. Perintah yang paling menarik adalah perintah yang menjalankan build: build, pengujian, run, cakupan, dan sebagainya: fungsi ini diimplementasikan oleh
BuildTool
.Kumpulan pola target di command line diuraikan dan karakter pengganti seperti
//pkg:all
dan//pkg/...
di-resolve. Hal ini diimplementasikan diAnalysisPhaseRunner.evaluateTargetPatterns()
dan diperbarui di Skyframe sebagaiTargetPatternPhaseValue
.Fase pemuatan/analisis dijalankan untuk menghasilkan grafik tindakan (grafik terarah asiklik yang perlu dijalankan untuk build).
Fase eksekusi dijalankan. Artinya, setiap tindakan yang diperlukan untuk mem-build target tingkat teratas yang diminta akan dijalankan.
Opsi command line
Opsi command line untuk pemanggilan Bazel dijelaskan dalam
objek OptionsParsingResult
, yang pada gilirannya berisi peta dari "class
opsi" ke nilai opsi. "Class opsi" adalah subclass dari
OptionsBase
dan mengelompokkan opsi command line yang terkait satu sama
lain. Contoh:
- Opsi yang terkait dengan bahasa pemrograman (
CppOptions
atauJavaOptions
). Opsi ini harus berupa subclassFragmentOptions
dan pada akhirnya digabungkan ke dalam objekBuildOptions
. - Opsi yang terkait dengan cara Bazel menjalankan tindakan (
ExecutionOptions
)
Opsi ini dirancang untuk digunakan dalam fase analisis dan (baik
melalui RuleContext.getFragment()
di Java maupun ctx.fragments
di Starlark).
Beberapa di antaranya (misalnya, apakah akan melakukan pemindaian C++ atau tidak) dibaca
dalam fase eksekusi, tetapi hal itu selalu memerlukan plumbing eksplisit karena
BuildConfiguration
tidak tersedia saat itu. Untuk informasi selengkapnya, lihat
bagian “Konfigurasi”.
PERINGATAN: Kita ingin berpura-pura bahwa instance OptionsBase
tidak dapat diubah dan
menggunakannya dengan cara itu (seperti sebagai bagian dari SkyKeys
). Ini tidak benar dan
mengubahnya adalah cara yang sangat baik untuk merusak Bazel dengan cara halus yang sulit
di-debug. Sayangnya, membuat data tersebut benar-benar tidak dapat diubah adalah upaya yang besar.
(Mengubah FragmentOptions
segera setelah konstruksi sebelum orang lain
mendapatkan kesempatan untuk menyimpan referensi ke FragmentOptions
dan sebelum equals()
atau hashCode()
dipanggil tidak masalah.)
Bazel mempelajari class opsi dengan cara berikut:
- Beberapa di antaranya terintegrasi dengan Bazel (
CommonCommandOptions
) - Dari anotasi @Command pada setiap perintah Bazel
- Dari
ConfiguredRuleClassProvider
(ini adalah opsi command line yang terkait dengan setiap bahasa pemrograman) - Aturan Starlark juga dapat menentukan opsinya 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 command line beserta beberapa teks bantuan.
Jenis Java dari nilai opsi command line biasanya sesuatu yang sederhana
(string, bilangan bulat, Boolean, label, dll.). Namun, kita juga mendukung
opsi jenis yang lebih rumit; dalam hal ini, tugas mengonversi dari
string command line ke jenis data bergantung pada implementasi
com.google.devtools.common.options.Converter
.
Pohon sumber, seperti yang terlihat oleh Bazel
Bazel bergerak di bidang pembuatan software, yang dilakukan dengan membaca dan menafsirkan kode sumber. Totalitas kode sumber yang digunakan Bazel disebut "ruang kerja" dan disusun ke dalam repositori, paket, dan aturan.
Repositori
"Repositori" adalah hierarki sumber tempat developer bekerja; biasanya mewakili satu project. Nenek moyang 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 "global" untuk seluruh
build, misalnya, kumpulan repositori eksternal yang tersedia. File ini berfungsi seperti file Starlark biasa, yang berarti Anda dapat 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 harus digabungkan; hal ini
dilakukan oleh SymlinkForest, yang membuat symlink setiap paket di repositori utama ke
$EXECROOT
dan setiap repositori eksternal ke $EXECROOT/external
atau
$EXECROOT/..
(yang pertama tentu saja membuat paket
yang disebut external
di repositori utama tidak mungkin; itulah sebabnya kita bermigrasi dari
repositori tersebut)
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
file BUILD
masih diterima adalah karena ancestor Bazel, Blaze, menggunakan nama
file ini. Namun, ternyata ini adalah segmen jalur yang umum digunakan, terutama
di Windows, tempat 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 file BUILD
_dapat _mengubah paket lain, karena glob rekursif berhenti di batas paket
sehingga kehadiran file BUILD
menghentikan rekursi.
Evaluasi file BUILD
disebut "pemuatan paket". Class 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
. Hal ini sebagian besar adalah peta dari string (nama
target) ke target itu sendiri.
Sebagian besar kompleksitas selama pemuatan paket adalah globbing: Bazel tidak
memerlukan setiap file sumber untuk dicantumkan secara eksplisit, tetapi dapat menjalankan glob
(seperti glob(["**/*.java"])
). Tidak seperti shell, Bazel mendukung glob rekursif yang
turun ke subdirektori (tetapi tidak ke subpaket). Hal ini memerlukan akses ke
sistem file dan karena dapat berjalan lambat, kami menerapkan berbagai trik untuk
membuatnya berjalan secara paralel dan seefisien mungkin.
Globbing diimplementasikan di class berikut:
LegacyGlobber
, globber yang cepat dan tidak mengetahui SkyframeSkyframeHybridGlobber
, versi yang menggunakan Skyframe dan kembali ke globber lama untuk menghindari “Skyframe dimulai ulang” (dijelaskan di bawah)
Class Package
itu sendiri berisi beberapa anggota yang secara eksklusif digunakan untuk
menguraikan file WORKSPACE dan tidak masuk akal untuk paket sebenarnya. Hal ini
merupakan kekurangan desain karena objek yang mendeskripsikan paket reguler tidak boleh berisi
kolom yang mendeskripsikan hal lain. Ini mencakup:
- Pemetaan repositori
- Toolchain terdaftar
- Platform eksekusi terdaftar
Idealnya, akan ada lebih banyak pemisahan antara mengurai 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:
- File: hal yang merupakan input atau output build. Dalam bahasa Bazel, kami menyebutnya artefak (dibahas di tempat lain). Tidak semua file yang dibuat selama build adalah target; output Bazel biasanya tidak memiliki label yang terkait.
- Aturan: ini menjelaskan langkah-langkah untuk mendapatkan output dari input-nya. Jenis ini
umumnya dikaitkan dengan bahasa pemrograman (seperti
cc_library
,java_library
, ataupy_library
), tetapi ada beberapa jenis yang tidak bergantung pada bahasa (sepertigenrule
ataufilegroup
) - 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 di command line, beberapa bagian label
dapat dihilangkan:
- Jika repositori dihilangkan, label akan dianggap berada di repositori utama.
- 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
diimplementasikan di Starlark (fungsi rule()
) atau di Java (disebut
"aturan native", ketik RuleClass
). Dalam jangka panjang, setiap aturan khusus bahasa
akan diimplementasikan di Starlark, tetapi beberapa keluarga 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 alami" diketahui oleh
Bazel, karena terdaftar dengan ConfiguredRuleClassProvider
.
Class aturan berisi informasi seperti:
- Atributnya (seperti
srcs
,deps
): jenis, nilai default, batasan, dll. - Transisi konfigurasi dan aspek yang dilampirkan ke setiap atribut, jika ada
- Penerapan aturan
- Penyedia info transitif yang "biasanya" dibuat oleh aturan
Catatan terminologi: Dalam code base, kita sering menggunakan “Aturan” untuk menunjukkan target
yang dibuat oleh class aturan. Namun, di Starlark dan dalam dokumentasi yang ditampilkan kepada pengguna,
"Aturan" harus digunakan secara eksklusif untuk merujuk ke class aturan itu sendiri; target
hanya berupa "target". Perhatikan juga bahwa meskipun RuleClass
memiliki "class" dalam
namanya, tidak ada hubungan pewarisan Java antara class aturan dan target
dari jenis tersebut.
Skyframe
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 berlaku
(seperti untuk setiap class opsi BuildOptions
, yang merupakan anggota
BuildConfigurationValue
dan SkyKey
-nya), kami berusaha 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 membuang grafik, satu SkyValue
per baris. Sebaiknya lakukan untuk build kecil, karena ukurannya bisa cukup besar.
Skyframe berada dalam paket com.google.devtools.build.skyframe
. Paket
com.google.devtools.build.lib.skyframe
yang bernama sama 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 berfungsi pada
tingkat perincian SkyFunction
dan SkyValue
.
Setiap kali SkyFunction
meminta dependensi yang tidak tersedia, getValue()
akan menampilkan null. Fungsi ini kemudian akan memberikan kontrol kembali ke Skyframe dengan menampilkan null. Pada suatu waktu nanti, Skyframe akan mengevaluasi dependensi yang tidak tersedia, lalu memulai ulang fungsi dari awal — hanya kali ini panggilan getValue()
akan berhasil dengan hasil non-null.
Konsekuensinya adalah setiap komputasi yang dilakukan di dalam SkyFunction
sebelum dimulai 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:
- Mendeklarasikan dependensi dalam batch (dengan menggunakan
getValuesAndExceptions()
) untuk membatasi jumlah mulai ulang. - Membagi
SkyValue
menjadi bagian terpisah yang dihitung olehSkyFunction
yang berbeda, sehingga dapat dihitung dan di-cache secara independen. Hal ini harus dilakukan secara strategis, karena berpotensi meningkatkan penggunaan memori. - 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 kita secara rutin memiliki ratusan ribu node Skyframe dalam penerbangan, dan Java tidak mendukung thread ringan.
Starlark
Starlark adalah bahasa khusus domain yang digunakan orang untuk mengonfigurasi dan memperluas Bazel. Python ini dianggap sebagai subset Python terbatas yang memiliki lebih sedikit jenis, lebih banyak batasan pada alur kontrol, dan yang paling penting, jaminan immutability yang kuat untuk memungkinkan pembacaan serentak. Bahasa ini tidak Turing-complete, yang mendorong beberapa (tetapi tidak semua) pengguna untuk mencoba menyelesaikan tugas pemrograman umum dalam bahasa tersebut.
Starlark diterapkan dalam paket net.starlark.java
.
Class ini juga memiliki implementasi Go independen
di sini. Implementasi Java
yang digunakan di Bazel saat ini adalah penafsir.
Starlark digunakan dalam beberapa konteks, termasuk:
- Bahasa
BUILD
. Di sinilah aturan baru ditentukan. Kode Starlark yang berjalan dalam konteks ini hanya memiliki akses ke konten fileBUILD
itu sendiri dan file.bzl
yang dimuat olehnya. - Definisi aturan. Ini adalah cara aturan baru (seperti dukungan untuk bahasa baru) ditentukan. Kode Starlark yang berjalan dalam konteks ini memiliki akses ke konfigurasi dan data yang disediakan oleh dependensi langsungnya (selengkapnya tentang hal ini nanti).
- File WORKSPACE. Di sinilah repositori eksternal (kode yang tidak ada dalam hierarki sumber utama) ditentukan.
- Definisi aturan repositori. Di sinilah jenis repositori eksternal baru ditentukan. Kode Starlark yang berjalan dalam konteks ini dapat menjalankan kode arbitrer di mesin tempat Bazel berjalan, dan menjangkau di luar ruang kerja.
Dialek yang tersedia untuk file BUILD
dan .bzl
sedikit berbeda
karena mengekspresikan hal yang berbeda. Daftar perbedaannya tersedia
di sini.
Informasi selengkapnya tentang Starlark tersedia di sini.
Fase pemuatan/analisis
Fase pemuatan/analisis adalah saat Bazel menentukan tindakan yang diperlukan untuk membuat aturan tertentu. Unit dasarnya adalah "target yang dikonfigurasi", yang, cukup wajar, merupakan pasangan (target, konfigurasi).
Fase ini disebut "fase pemuatan/analisis" karena dapat dibagi menjadi dua bagian berbeda, yang sebelumnya merupakan serial, tetapi kini bisa saling tumpang tindih dalam waktu:
- Memuat paket, yaitu mengubah file
BUILD
menjadi objekPackage
yang mewakilinya - Menganalisis target yang dikonfigurasi, yaitu menjalankan implementasi aturan untuk menghasilkan grafik tindakan
Setiap target yang dikonfigurasi dalam penutupan transitif target yang dikonfigurasi yang diminta di command line harus dianalisis dari bawah ke atas; yaitu, node daun terlebih dahulu, lalu hingga node di command line. Input untuk analisis satu target yang dikonfigurasi adalah:
- Konfigurasi. ("cara" membuat aturan tersebut; misalnya, platform target, tetapi juga hal-hal seperti opsi command line yang ingin diteruskan pengguna ke compiler C++)
- Dependensi langsung. Penyedia info transitifnya tersedia untuk aturan yang sedang dianalisis. Fungsi ini disebut demikian karena menyediakan "gabungan" informasi dalam penutupan transitif target yang dikonfigurasi, seperti semua file .jar di classpath atau semua file .o yang perlu ditautkan ke biner C++)
- Target itu sendiri. Ini adalah hasil pemuatan paket tempat target berada. Untuk aturan, ini mencakup atributnya, yang biasanya merupakan hal yang penting.
- Implementasi target yang dikonfigurasi. Untuk aturan, ini dapat berada di Starlark atau di Java. Semua target yang dikonfigurasi non-aturan diterapkan di Java.
Output analisis target yang dikonfigurasi adalah:
- Penyedia info transitif yang mengonfigurasi target yang bergantung padanya dapat mengakses
- Artefak yang dapat dibuatnya 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 Hal Buruk™, misalnya menulis kode yang kompleksitas waktu atau
ruangnya kuadrat (atau lebih buruk), membuat server Bazel error dengan
pengecualian Java, atau melanggar invarian (seperti dengan 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" membuat target: untuk platform apa, dengan opsi command line apa, dll.
Target yang sama dapat dibuat untuk beberapa konfigurasi dalam build yang sama. Hal ini berguna, misalnya ketika kode yang sama digunakan untuk alat yang dijalankan selama proses 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, dalam prakteknya, BuildOptions
digabungkan oleh BuildConfiguration
yang menyediakan berbagai fungsi tambahan. Perubahan ini diperluas dari bagian atas
grafik dependensi ke bagian 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 hal itu hanya memengaruhi target pengujian (kami memiliki rencana untuk "memotong" konfigurasi sehingga hal ini tidak terjadi, tetapi belum siap).
Jika implementasi aturan memerlukan bagian dari konfigurasi, implementasi tersebut 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 jika opsi Python berubah, target
C++ tidak perlu dianalisis ulang.
Konfigurasi aturan tidak harus sama dengan konfigurasi aturan "induk"-nya. Proses perubahan konfigurasi pada edge dependensi disebut "transisi konfigurasi". Hal ini dapat terjadi di dua tempat:
- Di tepi dependensi. Transisi ini ditentukan dalam
Attribute.Builder.cfg()
dan merupakan fungsi dariRule
(tempat transisi terjadi) danBuildOptions
(konfigurasi asli) ke satu atau beberapaBuildOptions
(konfigurasi output). - Di setiap edge masuk ke target yang dikonfigurasi. Ini ditentukan dalam
RuleClass.Builder.cfg()
.
Class yang relevan adalah TransitionFactory
dan ConfigurationTransition
.
Transisi konfigurasi digunakan, misalnya:
- Untuk mendeklarasikan bahwa dependensi tertentu digunakan selama build sehingga dependensi tersebut harus di-build dalam arsitektur eksekusi
- Untuk mendeklarasikan bahwa dependensi tertentu harus dibuat untuk beberapa arsitektur (seperti untuk kode native dalam APK Android fat)
Jika transisi konfigurasi menghasilkan beberapa konfigurasi, transisi tersebut disebut transisi terpisah.
Transisi konfigurasi juga dapat diterapkan di Starlark (dokumentasi di sini)
Penyedia info transitif
Penyedia info transitif adalah cara (dan _satu-satunya _cara) bagi target yang dikonfigurasi untuk memberi tahu hal-hal tentang target lain yang dikonfigurasi yang bergantung padanya. Alasan mengapa "transitive" ada dalam namanya adalah karena biasanya ini adalah semacam gabungan penutupan transitif dari target yang dikonfigurasi.
Umumnya ada korespondensi 1:1 antara penyedia info transitif Java
dan Starlark (pengecualian 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:
- Objek Class Java. Ini hanya tersedia untuk penyedia yang tidak dapat diakses dari Starlark. Penyedia ini adalah subclass
TransitiveInfoProvider
. - String. 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
. - Simbol penyedia. Ini dapat dibuat dari Starlark menggunakan fungsi
provider()
dan merupakan cara yang direkomendasikan untuk membuat penyedia baru. Simbol tersebut diwakili oleh instanceProvider.Key
di Java.
Penyedia baru yang diimplementasikan di Java harus diimplementasikan menggunakan BuiltinProvider
.
NativeProvider
tidak digunakan lagi (kami belum sempat menghapusnya) dan
subclass TransitiveInfoProvider
tidak dapat diakses dari Starlark.
Target yang dikonfigurasi
Target yang dikonfigurasi diterapkan sebagai RuleConfiguredTargetFactory
. Ada subclass untuk setiap class aturan yang diterapkan di Java. Target yang dikonfigurasi Starlark
dibuat melalui StarlarkRuleConfiguredTargetUtil.buildRule()
.
Factory target yang dikonfigurasi harus menggunakan RuleConfiguredTargetBuilder
untuk
membuat nilai yang ditampilkan. Isinya terdiri dari hal-hal berikut:
filesToBuild
-nya, konsep buram "kumpulan file yang diwakili aturan ini". Ini adalah file yang dibuat saat target yang dikonfigurasi ada di command line atau di src genrule.- Runfile, reguler, dan data.
- Grup outputnya. Ini adalah berbagai "kumpulan file lain" yang dapat dibuat
oleh aturan. Atribut ini dapat diakses menggunakan atribut output_group dari
aturan filegroup di BUILD dan menggunakan penyedia
OutputGroupInfo
di Java.
Runfile
Beberapa biner memerlukan file data untuk dijalankan. Contohnya adalah pengujian yang memerlukan file input. Hal ini direpresentasikan di Bazel dengan konsep "runfile". "Runfiles tree" adalah hierarki direktori file data untuk biner tertentu. File ini dibuat di sistem file sebagai hierarki symlink dengan setiap symlink yang mengarah ke file di sumber hierarki output.
Kumpulan runfile direpresentasikan sebagai instance Runfiles
. Secara konseptual, ini adalah
peta dari jalur file dalam hierarki runfile ke instance Artifact
yang
mewakilinya. Ini sedikit lebih rumit daripada satu Map
karena dua
alasan:
- Sering kali, jalur runfile file sama dengan execpath-nya. Kita menggunakannya untuk menghemat RAM.
- Ada berbagai jenis entri lama dalam hierarki runfile, yang juga perlu diwakili.
Runfile dikumpulkan menggunakan RunfilesProvider
: instance class ini
mewakili runfile target yang dikonfigurasi (seperti library) dan kebutuhan penutupan
transitipnya, dan dikumpulkan seperti kumpulan bertingkat (sebenarnya,
diimplementasikan menggunakan kumpulan bertingkat di balik layar): setiap target menggabungkan runfile
dependensinya, menambahkan beberapa runfile-nya sendiri, lalu mengirim kumpulan yang dihasilkan ke atas
dalam grafik dependensi. Instance RunfilesProvider
berisi dua instance Runfiles
, satu untuk saat aturan bergantung pada atribut "data" dan satu untuk setiap jenis dependensi masuk lainnya. Hal ini karena target
terkadang menyajikan runfile yang berbeda jika diandalkan melalui atribut data
daripada yang lain. Ini adalah perilaku lama yang tidak diinginkan dan belum kami hapus.
Runfile biner direpresentasikan sebagai instance RunfilesSupport
. Hal ini
berbeda dengan Runfiles
karena RunfilesSupport
memiliki kemampuan
untuk benar-benar dibuat (tidak seperti Runfiles
, yang hanya merupakan pemetaan). Tindakan ini
memerlukan komponen tambahan berikut:
- Manifes runfile input. Ini adalah deskripsi serial dari hierarki runfile. 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 oleh symlink. Untuk mengurangi jumlah tepi dependensi, perantara runfile dapat digunakan untuk mewakili semua ini.
- Argumen command line untuk menjalankan biner yang file run-nya mewakili
objek
RunfilesSupport
.
Aspek
Aspek adalah cara untuk "menyebarkan komputasi ke bawah grafik dependensi". Hal ini
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 ini direpresentasikan di Skyframe sebagai SkyValue
dan cara pembuatannya sangat mirip dengan cara pembuatan target yang dikonfigurasi: target ini memiliki class factory yang disebut ConfiguredAspectFactory
yang memiliki
akses ke RuleContext
, tetapi tidak seperti factory target yang dikonfigurasi, target ini juga mengetahui
target yang dikonfigurasi yang dilampirkan 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:
AspectClass
adalah implementasi aspek. Dapat berupa Java (dalam hal ini merupakan subclass) atau Starlark (dalam hal ini merupakan instanceStarlarkAspectClass
). Hal ini analog denganRuleConfiguredTargetFactory
.AspectDefinition
adalah definisi aspek; mencakup penyedia yang diperlukan, penyedia yang disediakan, dan berisi referensi ke implementasinya, seperti instanceAspectClass
yang sesuai. Ini analog denganRuleClass
.AspectParameters
adalah cara untuk memparametrisasi aspek yang disebarkan ke bawah grafik dependensi. Saat ini, ini adalah peta string ke string. Contoh yang baik tentang kegunaannya adalah buffering protokol: jika suatu bahasa memiliki beberapa API, informasi tentang API mana yang harus dibuat buffering protokolnya harus disebarkan ke bawah grafik dependensi.Aspect
mewakili semua data yang diperlukan untuk menghitung aspek yang menyebar ke bawah grafik dependensi. Class ini terdiri dari class aspek, definisinya, dan parameternya.RuleAspect
adalah fungsi yang menentukan aspek mana yang harus di-propagasi oleh aturan tertentu. Ini adalah fungsiRule
->Aspect
.
Komplikasi yang agak tidak terduga adalah aspek dapat dilampirkan ke aspek lain;
misalnya, aspek yang mengumpulkan classpath untuk IDE Java mungkin
ingin mengetahui semua file .jar di classpath, tetapi beberapa di antaranya adalah
buffer protokol. Dalam hal ini, aspek IDE akan ingin dilampirkan ke
pasangan (aturan proto_library
+ aspek proto Java).
Kompleksitas aspek pada aspek ditangkap dalam class
AspectCollection
.
Platform dan toolchain
Bazel mendukung build multi-platform, yaitu build yang mungkin memiliki beberapa arsitektur tempat tindakan build berjalan dan beberapa arsitektur untuk kode yang di-build. Arsitektur ini disebut sebagai platform dalam istilah 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). Kita memiliki "kamus" setelan dan nilai batasan yang paling umum digunakan di repositori @platforms
.
Konsep toolchain berasal dari fakta bahwa bergantung pada platform tempat build berjalan dan platform yang ditargetkan, Anda mungkin perlu menggunakan compiler yang berbeda; misalnya, toolchain C++ tertentu dapat berjalan di OS tertentu dan dapat menargetkan beberapa OS lainnya. Bazel harus menentukan compiler C++ yang digunakan berdasarkan platform target dan eksekusi yang ditetapkan (dokumentasi untuk toolchain di sini).
Untuk melakukannya, toolchain dianotasikan dengan kumpulan batasan platform eksekusi dan target yang didukungnya. Untuk melakukannya, definisi toolchain dibagi menjadi dua bagian:
- 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 aturantoolchain_type()
) - Aturan khusus bahasa yang menjelaskan toolchain sebenarnya (seperti
cc_toolchain()
)
Hal ini dilakukan dengan cara ini karena kita perlu mengetahui batasan untuk setiap
toolchain guna melakukan resolusi toolchain dan aturan
*_toolchain()
khusus bahasa berisi lebih banyak informasi daripada itu, sehingga memerlukan lebih banyak
waktu untuk dimuat.
Platform eksekusi ditentukan dengan salah satu cara berikut:
- Dalam file WORKSPACE menggunakan fungsi
register_execution_platforms()
- Di command line menggunakan opsi command line --extra_execution_platforms
Kumpulan platform eksekusi yang tersedia dihitung di
RegisteredExecutionPlatformsFunction
.
Platform target untuk target yang dikonfigurasi ditentukan oleh
PlatformOptions.computeTargetPlatform()
. Ini adalah daftar platform karena pada akhirnya
kami ingin mendukung beberapa platform target, tetapi belum diterapkan
secara penuh.
Kumpulan toolchain yang akan digunakan untuk target yang dikonfigurasi ditentukan oleh
ToolchainResolutionFunction
. Fungsi ini adalah fungsi dari:
- Kumpulan toolchain terdaftar (dalam file WORKSPACE dan konfigurasi)
- Platform target dan eksekusi yang diinginkan (dalam konfigurasi)
- Kumpulan jenis toolchain yang diperlukan oleh target yang dikonfigurasi (di
UnloadedToolchainContextKey)
- Kumpulan batasan platform eksekusi target yang dikonfigurasi (atribut
exec_compatible_with
) dan konfigurasi (--experimental_add_exec_constraints_to_targets
), diUnloadedToolchainContextKey
Hasilnya adalah UnloadedToolchainContext
, yang pada dasarnya merupakan peta dari
jenis toolchain (direpresentasikan sebagai instance ToolchainTypeInfo
) ke label
rantai alat yang dipilih. Ini disebut "di-unload" 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 konfigurasi "host"
dan konfigurasi target yang direpresentasikan oleh berbagai
flag konfigurasi, seperti --cpu
. Kami secara bertahap bertransisi ke sistem
di atas. Untuk menangani kasus saat pengguna mengandalkan nilai konfigurasi
lama, kami telah menerapkan
pemetaan platform
untuk menerjemahkan antara flag lama dan batasan platform gaya baru.
Kodenya berada di 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 ini
akan dihapus dan tidak tersedia di Bazel, tetapi kode sumber mungkin
berisi referensi ke batasan 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" tempat aturan tersebut dapat dibuat, dengan
"lingkungan" adalah instance aturan environment()
.
Ada berbagai cara untuk menentukan lingkungan yang didukung untuk aturan:
- Melalui atribut
restricted_to=
. Ini adalah bentuk spesifikasi yang paling langsung; spesifikasi ini mendeklarasikan kumpulan lingkungan yang tepat yang didukung aturan untuk grup ini. - Melalui atribut
compatible_with=
. Ini mendeklarasikan lingkungan yang didukung aturan selain lingkungan "standar" yang didukung secara default. - Melalui atribut tingkat paket
default_restricted_to=
dandefault_compatible_with=
. - 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 dari lingkungan ini yang harus didukung oleh "default" jika tidak ditentukan oleh atributrestricted_to=
/environment()
. Aturan tanpa atribut tersebut akan mewarisi semua default. - Melalui class aturan default. Tindakan ini akan mengganti default global untuk semua
instance 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 dari Target
, tetapi bukan Rule
(EnvironmentGroup
) dan
fungsi yang tersedia secara default dari Starlark
(StarlarkLibrary.environmentGroup()
) yang pada akhirnya membuat target
dengan nama yang sama. Hal ini untuk menghindari dependensi siklus yang akan muncul karena setiap
lingkungan perlu mendeklarasikan grup lingkungan tempatnya berada dan setiap
grup lingkungan perlu 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. Perubahan ini sedang ditinjau dalam permintaan pull #10945.
Visibilitas
Jika Anda mengerjakan codebase besar dengan banyak developer (seperti di Google), Anda harus berhati-hati untuk mencegah orang lain bergantung secara sewenang-wenang pada kode Anda. Jika tidak, sesuai dengan hukum Hyrum, orang akan mengandalkan perilaku yang Anda anggap sebagai detail penerapan.
Bazel mendukung hal ini dengan mekanisme yang disebut visibilitas: Anda dapat mendeklarasikan bahwa target tertentu hanya dapat diandalkan 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 kekurangan desain.)
Hal ini diterapkan di tempat berikut:
- Antarmuka
RuleVisibility
mewakili deklarasi visibilitas. Nilai ini 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 diimplementasikan sebagai targetnya sendiri (
PackageGroup
) dan target yang dikonfigurasi (PackageGroupConfiguredTarget
). Kita mungkin dapat mengganti ini dengan aturan sederhana jika ingin. Logikanya diterapkan dengan bantuan:PackageSpecification
, yang sesuai dengan satu pola seperti//pkg/...
;PackageGroupContents
, yang sesuai dengan satu atributpackages
package_group
; danPackageSpecificationProvider
, yang menggabungkanpackage_group
danincludes
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 filenya sendiri, dan menggabungkan kumpulan agregat ke dalam 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 melakukannya dengan cara naif menggunakan, misalnya, List
atau Set
, kita akan mendapatkan
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 menemukan konsep
NestedSet
. Ini adalah struktur data yang terdiri dari instance NestedSet
lain dan beberapa anggotanya sendiri, sehingga membentuk grafik acyclic
yang diarahkan dari kumpulan. Objek ini tidak dapat diubah dan anggotanya dapat di-iterasi. 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 sebenarnya terdiri dari serangkaian perintah yang perlu dijalankan untuk menghasilkan
output yang diinginkan pengguna. Perintah direpresentasikan sebagai instance
class Action
dan file direpresentasikan sebagai instance class
Artifact
. Elemen ini diatur dalam grafik acyclic bipartite yang diarahkan dan 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:
- **Artefak reguler. **File ini diperiksa keaktualannya dengan menghitung checksum-nya, dengan mtime sebagai pintasan; kami tidak melakukan checksum pada file jika ctime-nya belum berubah.
- Artefak symlink yang belum terselesaikan. Hal ini diperiksa keaktualannya dengan memanggil readlink(). Tidak seperti artefak biasa, ini dapat berupa symlink yang tidak tersambung. Biasanya digunakan jika seseorang kemudian memaketkan beberapa file ke dalam suatu arsip.
- Artefak hierarki. Ini bukan file tunggal, tetapi hierarki direktori. File tersebut
diperiksa keaktualannya dengan memeriksa kumpulan file di dalamnya dan
kontennya. Objek tersebut direpresentasikan sebagai
TreeArtifact
. - Artefak metadata konstan. Perubahan pada artefak ini tidak memicu pembuatan ulang. Ini digunakan secara eksklusif untuk informasi stempel build: kita tidak ingin melakukan build ulang hanya karena waktu saat ini berubah.
Tidak ada alasan mendasar mengapa artefak sumber tidak dapat berupa artefak hierarki atau
artefak symlink yang belum terselesaikan, hanya saja kita belum menerapkannya (tetapi kita
harus melakukannya -- mereferensikan direktori sumber dalam file BUILD
adalah salah satu
dari sedikit masalah kesalahan lama yang diketahui dengan Bazel; kita memiliki
implementasi yang berfungsi dan diaktifkan oleh
properti JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1
)
Jenis Artifact
yang terkenal adalah perantara. Hal ini ditunjukkan oleh instance Artifact
yang merupakan output dari MiddlemanAction
. Fungsi ini digunakan untuk
menangani beberapa hal secara khusus:
- Agregasi perantara digunakan untuk mengelompokkan artefak. Hal ini dilakukan agar jika banyak tindakan menggunakan kumpulan input besar yang sama, kita tidak memiliki tepi dependensi N*M, hanya N+M (digantikan dengan kumpulan bertingkat)
- Penjadwalan perantara dependensi memastikan bahwa tindakan berjalan sebelum tindakan lainnya.
Ini sebagian besar digunakan untuk linting, tetapi juga untuk kompilasi C++ (lihat
CcCompilationContext.createMiddleman()
untuk penjelasan) - Perantara runfile digunakan untuk memastikan keberadaan hierarki runfile sehingga tidak perlu bergantung secara terpisah pada manifes output dan setiap artefak yang dirujuk oleh hierarki runfile.
Tindakan paling baik dipahami sebagai perintah yang perlu dijalankan, lingkungan yang diperlukan, dan kumpulan output yang dihasilkan. Hal-hal berikut adalah komponen utama deskripsi tindakan:
- Command line yang perlu dijalankan
- Artefak input yang diperlukan
- Variabel lingkungan yang perlu ditetapkan
- Anotasi yang mendeskripsikan lingkungan (seperti platform) tempatnya harus dijalankan \
Ada juga beberapa kasus khusus lainnya, seperti menulis file yang kontennya
diketahui oleh Bazel. Keduanya adalah subclass dari AbstractAction
. Sebagian besar tindakan adalah
SpawnAction
atau StarlarkAction
(sama, tindakan ini seharusnya tidak
merupakan class terpisah), meskipun Java dan C++ memiliki jenis tindakannya sendiri
(JavaCompileAction
, CppCompileAction
, dan CppLinkAction
).
Pada akhirnya, kita ingin memindahkan semuanya ke SpawnAction
; JavaCompileAction
cukup mirip, tetapi C++ sedikit merupakan kasus khusus karena penguraian file .d dan
penyematan pemindaian.
Grafik tindakan sebagian besar "disematkan" ke dalam grafik Skyframe: secara konseptual,
eksekusi tindakan direpresentasikan sebagai pemanggilan
ActionExecutionFunction
. Pemetaan dari tepi dependensi grafik tindakan ke
tepi dependensi Skyframe dijelaskan dalam
ActionExecutionFunction.getInputDeps()
dan Artifact.key()
serta memiliki beberapa
pengoptimalan untuk menjaga jumlah tepi Skyframe tetap rendah:
- Artefak turunan tidak memiliki
SkyValue
-nya sendiri. Sebagai gantinya,Artifact.getGeneratingActionKey()
digunakan untuk mengetahui kunci untuk tindakan yang membuatnya - Set bertingkat memiliki kunci Skyframe-nya sendiri.
Tindakan bersama
Beberapa tindakan dihasilkan oleh beberapa target yang dikonfigurasi; Aturan Starlark lebih terbatas karena hanya diizinkan untuk menempatkan tindakan turunannya ke dalam direktori yang ditentukan oleh konfigurasi dan paketnya (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 misfeature, 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). Hal ini memerlukan biaya beberapa RAM: setiap instance tindakan bersama harus disimpan dalam memori secara terpisah.
Jika dua tindakan menghasilkan file output yang sama, tindakan tersebut harus sama persis:
memiliki input yang sama, output yang sama, dan menjalankan command line yang sama. Hubungan
ekivalensi ini diimplementasikan di Actions.canBeShared()
dan
diverifikasi antara fase analisis dan eksekusi dengan melihat setiap Action.
Hal ini diterapkan di SkyframeActionExecutor.findAndStoreArtifactConflicts()
dan merupakan salah satu dari sedikit tempat di Bazel yang memerlukan tampilan "global"
build.
Fase eksekusi
Ini adalah saat Bazel benar-benar mulai menjalankan tindakan build, seperti perintah yang menghasilkan output.
Hal pertama yang dilakukan Bazel setelah fase analisis adalah menentukan
Artefak yang perlu dibuat. Logika untuk ini dienkode dalam
TopLevelArtifactHelper
; secara kasar, ini adalah filesToBuild
dari
target yang dikonfigurasi di command line dan konten grup output
khusus untuk tujuan eksplisit dalam mengekspresikan "jika target ini ada di command
line, build artefak ini".
Langkah berikutnya adalah membuat root eksekusi. Karena Bazel memiliki opsi untuk membaca
paket sumber dari berbagai lokasi dalam sistem file (--package_path
),
Bazel perlu menyediakan tindakan yang dijalankan secara lokal dengan hierarki sumber lengkap. Hal ini
ditangani oleh class SymlinkForest
dan berfungsi dengan mencatat setiap target
yang digunakan dalam fase analisis dan membuat satu hierarki direktori yang membuat symlink
setiap paket dengan target yang digunakan dari lokasi sebenarnya. Alternatifnya adalah meneruskan jalur yang benar ke perintah (dengan mempertimbangkan --package_path
).
Hal ini tidak diinginkan karena:
- Tindakan ini mengubah command line tindakan saat paket dipindahkan dari entri jalur paket ke entri lainnya (biasanya terjadi secara umum)
- Hal ini menghasilkan command line yang berbeda jika tindakan dijalankan dari jarak jauh dibandingkan jika dijalankan secara lokal
- Hal ini memerlukan transformasi command line khusus untuk alat yang digunakan (pertimbangkan perbedaan antara classpath Java dan jalur penyertaan C++)
- Mengubah command line tindakan akan membatalkan entri cache tindakannya
--package_path
perlahan-lahan tidak digunakan lagi
Kemudian, Bazel mulai menjelajahi grafik tindakan (grafik terarah bipartit
yang terdiri dari tindakan serta artefak input dan outputnya) dan menjalankan tindakan.
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 ulangActionExecutionFunction
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 dijalankan 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, Anda bisa mendapatkan hit cache tindakan lokal meskipun grafik Skyframe kosong.
Cache ini diperiksa untuk menemukan hit menggunakan metode
ActionCacheChecker.getTokenIfNeedToExecute()
.
Berbeda dengan namanya, ini adalah peta dari jalur artefak turunan ke tindakan yang memunculkannya. Tindakan ini dijelaskan sebagai:
- Kumpulan file input dan output serta checksum-nya
- "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 yang masih dalam pengembangan, yang menggunakan hash transitif untuk menghindari membuka cache sebanyak mungkin.
Penemuan input dan pemangkasan input
Beberapa tindakan lebih rumit daripada sekadar memiliki kumpulan input. Perubahan pada kumpulan input tindakan memiliki dua bentuk:
- 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 tepat tentang file header yang digunakan file
C++ dari penutupan transitifnya sehingga kita tidak perlu mengirim setiap
file ke eksekutor jarak jauh; oleh karena itu, kita memiliki opsi untuk tidak mendaftarkan setiap
file header sebagai "input", tetapi memindai file sumber untuk header yang disertakan secara
transitif dan hanya menandai file header tersebut sebagai input yang
disebutkan dalam pernyataan
#include
(kita melebih-lebihkan sehingga kita tidak perlu menerapkan preprocessor C lengkap) Opsi ini saat ini di-hardwire ke "false" di Bazel dan hanya digunakan di Google. - Tindakan mungkin menyadari bahwa beberapa file tidak digunakan selama eksekusi. Di C++, ini disebut "file .d": compiler memberi tahu file header mana yang digunakan setelah fakta, dan untuk menghindari rasa malu karena memiliki inkrementalitas yang lebih buruk daripada Make, Bazel memanfaatkan fakta ini. Hal ini menawarkan perkiraan yang lebih baik daripada pemindai include karena bergantung pada compiler.
Ini diimplementasikan menggunakan metode pada Action:
Action.discoverInputs()
dipanggil. Fungsi ini akan menampilkan kumpulan Artefak bertingkat yang ditentukan sebagai diperlukan. 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.- Tindakan ini dijalankan dengan memanggil
Action.execute()
. - Di akhir
Action.execute()
, tindakan dapat memanggilAction.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 memanggil updateInputs()
itu sendiri sehingga kumpulan
input mencerminkan hasil penemuan dan pemangkasan input yang dilakukan sebelumnya.
Tindakan Starlark dapat memanfaatkan fasilitas untuk mendeklarasikan beberapa input sebagai tidak digunakan
menggunakan argumen unused_inputs_list=
dari
ctx.actions.run()
.
Berbagai cara untuk menjalankan tindakan: Strategi/ActionContexts
Beberapa tindakan dapat dijalankan dengan cara yang berbeda. Misalnya, command line dapat dijalankan secara lokal, lokal, tetapi dalam berbagai jenis sandbox, atau dari jarak jauh. Konsep
yang mewujudkan hal ini disebut ActionContext
(atau Strategy
, karena kita
hanya berhasil melakukan penggantian nama setengah jalan...)
Siklus proses konteks tindakan adalah sebagai berikut:
- Saat fase eksekusi dimulai, instance
BlazeModule
ditanya tentang konteks tindakan yang mereka miliki. Hal ini terjadi di konstruktorExecutionTool
. Jenis konteks tindakan diidentifikasi oleh instanceClass
Java yang merujuk ke sub-antarmukaActionContext
dan antarmuka yang harus diimplementasikan oleh konteks tindakan. - Konteks tindakan yang sesuai akan dipilih dari tindakan yang tersedia, serta diteruskan ke
ActionExecutionContext
danBlazeExecutor
. - Tindakan meminta konteks menggunakan
ActionExecutionContext.getContext()
danBlazeExecutor.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 strategi yang selesai lebih dulu.
Salah satu strategi penting adalah metode yang mengimplementasikan proses pekerja yang persisten (WorkerSpawnStrategy
). Idenya adalah beberapa alat memiliki waktu startup yang lama sehingga harus digunakan kembali antartindakan, bukan memulai yang baru untuk setiap tindakan (Ini merupakan potensi masalah ketepatan, karena Bazel bergantung pada janji proses pekerja bahwa alat tersebut tidak membawa status yang dapat diamati antar-permintaan individu)
Jika alat berubah, proses pekerja harus dimulai ulang. Apakah pekerja
dapat digunakan kembali ditentukan dengan menghitung checksum untuk alat yang digunakan menggunakan
WorkerFilesHash
. Hal ini bergantung pada mengetahui input tindakan mana yang mewakili
bagian alat dan mana yang mewakili input; hal ini ditentukan oleh pembuat
Tindakan: 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, yaitu strategi yang menjalankan tindakan secara lokal dan jarak jauh untuk melihat mana yang selesai lebih dulu tersedia di sini.
- Informasi tentang kerumitan 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-beda dari tindakan ke tindakan: semakin banyak resource yang diperlukan tindakan, semakin sedikit instance yang harus berjalan secara bersamaan untuk menghindari overload pada 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 pengelolaan resource lokal yang lebih mendetail tersedia di sini.
Struktur direktori output
Setiap tindakan memerlukan tempat terpisah di direktori output tempat tindakan tersebut menempatkan output-nya. Lokasi artefak turunan biasanya sebagai berikut:
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
Bagaimana nama direktori yang dikaitkan dengan konfigurasi tertentu ditentukan? Ada dua properti yang diinginkan yang bertentangan:
- Jika dua konfigurasi dapat terjadi dalam build yang sama, keduanya harus memiliki direktori yang berbeda sehingga keduanya dapat memiliki versi tindakan yang sama; jika tidak, jika kedua konfigurasi tidak setuju, seperti command line tindakan yang menghasilkan file output yang sama, Bazel tidak tahu tindakan mana yang harus dipilih ("konflik tindakan")
- Jika dua konfigurasi mewakili "secara kasar" hal yang sama, keduanya harus memiliki nama yang sama sehingga tindakan yang dijalankan di satu konfigurasi dapat digunakan kembali untuk konfigurasi lainnya jika baris perintah cocok: misalnya, perubahan pada opsi command line ke compiler Java tidak boleh menyebabkan tindakan kompilasi C++ dijalankan kembali.
Sejauh ini, kita belum menemukan cara yang prinsipil untuk menyelesaikan masalah ini, yang memiliki kesamaan dengan masalah pemangkasan konfigurasi. Diskusi lebih lanjut tentang opsi tersedia di sini. Area utama yang bermasalah adalah aturan Starlark (yang penulisnya biasanya tidak sangat memahami Bazel) dan aspek, yang menambahkan dimensi lain ke ruang hal-hal yang dapat menghasilkan file output "sama".
Pendekatan saat ini adalah segmen jalur untuk konfigurasi adalah
<CPU>-<compilation mode>
dengan berbagai akhiran yang ditambahkan sehingga transisi
konfigurasi yang diterapkan di Java tidak menyebabkan konflik tindakan. Selain itu, checksum kumpulan transisi konfigurasi Starlark ditambahkan sehingga pengguna
tidak dapat menyebabkan konflik tindakan. Hal 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 mendeflaking atau mengumpulkan data waktu)
- Pengujian sharding (membagi kasus pengujian dalam pengujian yang sama di 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 proses build-nya menghasilkan pengujian yang dijalankan. Ini adalah file "status
cache" yang berisi pesan
TestResultData
serial - Frekuensi pengujian harus dijalankan
- Jumlah shard yang akan dibagi untuk 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. Ekspansi
diimplementasikan di TestsForTargetPatternFunction
. Hal yang agak
mengejutkan adalah jika rangkaian pengujian tidak mendeklarasikan pengujian, rangkaian pengujian tersebut merujuk pada
setiap pengujian dalam paketnya. Hal ini diterapkan di Package.beforeBuild()
dengan
menambahkan atribut implisit yang disebut $implicit_tests
ke aturan suite pengujian.
Kemudian, pengujian difilter berdasarkan 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
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 akan difilter dan pengujian akan 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 akan dilakukan, sehingga mungkin menyimpang dari yang disebutkan di atas dalam
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 yang rumit yang menggunakan variabel lingkungan untuk memberi tahu pengujian apa yang diharapkan darinya. Deskripsi mendetail tentang hal yang diharapkan Bazel dari pengujian dan hal yang dapat diharapkan pengujian dari Bazel tersedia di sini. Secara sederhana, kode keluar 0 berarti berhasil, yang lainnya berarti gagal.
Selain file status cache, setiap proses pengujian menghasilkan sejumlah file
lain. Log tersebut ditempatkan di "direktori log pengujian" yang merupakan subdirektori yang disebut
testlogs
dari direktori output konfigurasi target:
test.xml
, file XML bergaya JUnit yang menjelaskan setiap kasus pengujian dalam shard pengujiantest.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 secara 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 diterapkan 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 pengujian
eksklusif sehingga output dari pengujian yang berbeda tidak diselingi.
Ini diterapkan di class StreamedTestOutput
yang dinamai dengan tepat dan berfungsi dengan
melakukan polling perubahan pada file test.log
pengujian yang dimaksud dan membuang 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 pengujian digabungkan dalam skrip yang disebut
collect_coverage.sh
.
Skrip ini menyiapkan lingkungan pengujian untuk mengaktifkan pengumpulan cakupan dan menentukan tempat file cakupan ditulis oleh runtime cakupan. Kemudian, pengujian akan dijalankan. Pengujian 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 ke
nilai flag konfigurasi --coverage_support
(lihat
TestConfiguration.TestOptions.coverageSupport
)
Beberapa bahasa melakukan instrumentasi offline, yang berarti bahwa instrumentasi cakupan ditambahkan pada waktu kompilasi (seperti C++) dan yang lainnya melakukan instrumentasi online, yang berarti bahwa instrumentasi cakupan ditambahkan pada waktu eksekusi.
Konsep inti lainnya adalah cakupan dasar pengukuran. Ini adalah cakupan library,
biner, atau pengujian 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 pengujian karena mungkin ada kode dalam biner yang tidak
ditautkan ke pengujian apa pun. Oleh karena itu, yang kita lakukan adalah memunculkan file cakupan untuk setiap
biner yang hanya berisi file yang cakupannya kita kumpulkan tanpa baris
yang tercakup. File cakupan dasar pengukuran untuk target adalah ukuran
bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
. File ini juga dibuat
untuk biner dan library selain pengujian jika Anda meneruskan
tanda --nobuild_tests_only
ke Bazel.
Cakupan dasar saat ini rusak.
Kami melacak dua grup file untuk pengumpulan cakupan untuk setiap aturan: kumpulan file berinstrumen dan kumpulan file metadata instrumentasi.
Kumpulan file berinstrumen hanyalah kumpulan file yang akan diinstrumentasi. Untuk runtime cakupan online, ini dapat digunakan saat runtime untuk menentukan file mana yang akan diinstrumentasi. Ini juga digunakan untuk menerapkan cakupan dasar pengukuran.
Kumpulan file metadata instrumentasi adalah kumpulan file tambahan yang diperlukan pengujian untuk membuat file LCOV yang diperlukan Bazel. 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 praktis 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 menghasilkan kode yang dapat mengumpulkan cakupan,
yang sedikit mengurangi masalah ini, karena analisis ulang tetap diperlukan).
File dukungan cakupan bergantung pada label pada dependensi implisit sehingga dapat diganti oleh kebijakan pemanggilan, yang memungkinkannya berbeda di antara berbagai versi Bazel. Idealnya, perbedaan ini akan dihapus, dan kami menstandarkan salah satunya.
Kita 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()
. Alat ini
mendapatkan akses ke alat yang diperlukan dengan melihat atribut :coverage_report_generator
pengujian pertama yang dieksekusi.
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 targetbazel cquery
digunakan untuk menyelidiki grafik target yang dikonfigurasibazel aquery
digunakan untuk menyelidiki grafik tindakan
Masing-masing ini 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 ditampilkan dengan berbagai cara: label, label dan class
aturan, XML, protobuf, dan sebagainya. Fungsi ini diimplementasikan sebagai subclass
OutputFormatter
.
Persyaratan halus dari beberapa format output kueri (proto, pasti) adalah bahwa Bazel perlu memunculkan _semua _informasi yang disediakan pemuatan paket sehingga seseorang dapat membandingkan 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 yang biasa digunakan adalah menggunakan label, dan melampirkan informasi 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 dulunya
disebut Blaze) dan mendapatkan informasi tentang berbagai peristiwa selama eksekusi
perintah.
Fungsi ini sebagian besar digunakan untuk menerapkan 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
Kumpulan titik ekstensi yang ditawarkan BlazeModule
agak acak. 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 direpresentasikan sebagai peristiwa:
- Daftar target build yang akan dibuat telah ditentukan
(
TargetParsingCompleteEvent
) - Konfigurasi tingkat teratas telah ditentukan
(
BuildConfigurationEvent
) - Target dibuat, berhasil atau tidak (
TargetCompleteEvent
) - Pengujian dijalankan (
TestAttempt
,TestSummary
)
Beberapa peristiwa ini direpresentasikan di luar Bazel dalam
Build Event Protocol
(peristiwa ini adalah 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 (hierarki sumber tunggal yang berisi semua yang perlu di-build), Bazel berada di dunia yang tidak selalu benar. "Repositori eksternal" adalah abstraksi yang digunakan untuk menjembatani kedua dunia ini: repositori ini 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")
Hasil di 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
) dibagi menjadi beberapa bagian yang dibatasi oleh pernyataan
load()
. Indeks bagian 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
diambil. Tindakan ini akan membuat Bazel membuat direktori di
$OUTPUT_BASE/external/<repository name>
.
Mengambil repositori dilakukan dalam langkah-langkah berikut:
PackageLookupFunction
menyadari bahwa ia memerlukan repositori dan membuatRepositoryName
sebagaiSkyKey
, yang memanggilRepositoryLoaderFunction
RepositoryLoaderFunction
meneruskan permintaan keRepositoryDelegatorFunction
karena alasan yang tidak jelas (kode mengatakan untuk menghindari download ulang jika Skyframe dimulai ulang, tetapi itu bukan alasan yang sangat kuat)RepositoryDelegatorFunction
mengetahui aturan repositori yang diminta untuk diambil dengan melakukan iterasi pada potongan file WORKSPACE sampai repositori yang diminta ditemukan- 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 penyimpanan ke cache karena mengambil repositori dapat sangat mahal:
- Ada cache untuk file yang didownload yang diberi kunci oleh checksumnya
(
RepositoryCache
). Hal ini mengharuskan checksum tersedia di file WORKSPACE, tetapi hal ini baik untuk hermetisitas. Hal ini digunakan bersama oleh setiap instance server Bazel di workstation yang sama, terlepas dari ruang kerja atau basis output tempat mereka berjalan. - "File penanda" ditulis untuk setiap repositori di bagian
$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 diRepositoryDelegatorFunction.DigestWriter
. - Opsi command line
--distdir
menetapkan cache lain yang digunakan untuk mencari artefak yang akan didownload. Hal ini berguna di setelan perusahaan tempat Bazel tidak boleh mengambil hal-hal acak dari Internet. Hal ini diimplementasikan olehDownloadManager
.
Setelah repositori didownload, artefak di dalamnya akan diperlakukan sebagai artefak
sumber. Hal ini menimbulkan masalah karena Bazel biasanya memeriksa keaktualan
artefak sumber dengan memanggil stat() pada artefak tersebut, dan artefak ini juga
dibuat tidak valid saat definisi repositori tempatnya berada berubah. 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 di root ruang kerja (seperti pengelola paket yang menyimpan paket yang didownload di subdirektori hierarki sumber). Hal ini bertentangan dengan asumsi yang dibuat Bazel bahwa file sumber hanya diubah oleh pengguna, bukan oleh dirinya sendiri, dan memungkinkan paket merujuk ke setiap direktori di root ruang kerja. Agar repositori eksternal semacam ini berfungsi, Bazel melakukan dua hal:
- Mengizinkan pengguna menentukan subdirektori ruang kerja yang tidak
diizinkan untuk dijangkau oleh Bazel. Fungsi tersebut tercantum dalam file yang disebut
.bazelignore
dan fungsinya diterapkan diBlacklistedPackagePrefixesFunction
. - Kita mengenkode pemetaan dari subdirektori ruang kerja ke repositori
eksternal yang menanganinya ke dalam
ManagedDirectoriesKnowledge
dan menanganiFileStateValue
yang merujuk ke repositori tersebut dengan cara yang sama seperti untuk repositori eksternal reguler.
Pemetaan repositori
Terkadang beberapa repositori ingin bergantung pada repositori yang sama,
tetapi dalam versi yang berbeda (ini adalah contoh "masalah dependensi berlian"). Misalnya, jika dua biner dalam repositori terpisah dalam build ingin bergantung pada Guava, keduanya mungkin akan merujuk ke Guava dengan label yang dimulai dari @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 menggabungkan 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 setiap definisi repositori. Kemudian, ia muncul di Skyframe sebagai anggota
WorkspaceFileValue
, tempat ia dihubungkan ke:
Package.Builder.repositoryMapping
yang digunakan untuk mengubah atribut aturan bernilai label dalam paket denganRuleClass.populateRuleAttributeValues()
Package.repositoryMapping
yang digunakan dalam fase analisis (untuk menyelesaikan hal-hal seperti$(location)
yang tidak diuraikan dalam fase pemuatan)BzlLoadFunction
untuk me-resolve label dalam pernyataan load()
Bit JNI
Server Bazel sebagian besar ditulis dalam Java. Pengecualian adalah bagian yang tidak dapat dilakukan Java sendiri atau tidak dapat dilakukan sendiri saat kita menerapkannya. Hal ini sebagian besar terbatas pada interaksi dengan sistem file, kontrol proses, dan berbagai hal tingkat rendah lainnya.
Kode C++ berada di src/main/native dan class Java dengan metode native adalah:
NativePosixFiles
danNativePosixFileSystem
ProcessUtils
WindowsFileOperations
danWindowsFileProcesses
com.google.devtools.build.lib.platform
Output konsol
Memunculkan output konsol tampaknya merupakan hal yang sederhana, tetapi gabungan dari beberapa proses yang berjalan (terkadang dari jarak jauh), caching terperinci, keinginan untuk memiliki output terminal yang bagus dan berwarna-warni, serta memiliki server yang berjalan lama membuatnya tidak biasa.
Tepat setelah panggilan RPC masuk dari klien, dua instance RpcOutputStream
dibuat (untuk stdout dan stderr) yang meneruskan data yang dicetak ke
klien. Kemudian, keduanya digabungkan dalam OutErr
(pasangan (stdout, stderr)). Semua yang perlu dicetak di konsol akan melalui streaming
ini. Kemudian, streaming ini akan diserahkan ke
BlazeCommandDispatcher.execExclusively()
.
Output secara default dicetak dengan urutan escape ANSI. Jika tidak
diinginkan (--color=no
), atribut tersebut akan dihapus oleh AnsiStrippingOutputStream
. Selain itu, System.out
dan System.err
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 suatu proses
menghasilkan output biner (seperti bazel query --output=proto
), munging stdout
tidak akan dilakukan.
Pesan singkat (error, peringatan, dan sejenisnya) dinyatakan melalui
antarmuka EventHandler
. Secara khusus, ini berbeda dengan yang diposting ke
EventBus
(ini membingungkan). Setiap Event
memiliki EventKind
(error,
peringatan, info, dan beberapa lainnya) dan mungkin memiliki Location
(tempat dalam
kode sumber yang menyebabkan peristiwa terjadi).
Beberapa implementasi EventHandler
menyimpan peristiwa yang 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 dan di-cache.
Beberapa EventHandler
juga mengizinkan peristiwa posting yang pada akhirnya menemukan jalan 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 peristiwa melakukannya; tetapi tidak diberlakukan)
Output terminal sebagian besar dikeluarkan melalui UiEventHandler
, yang
bertanggung jawab atas semua pemformatan output yang menarik dan pelaporan progres yang dilakukan
Bazel. Model ini memiliki dua input:
- Bus peristiwa
- Aliran peristiwa yang disalurkan ke dalamnya 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 saat perintah perlu membuang kemungkinan data biner dalam jumlah besar (seperti bazel query
).
Membuat profil Bazel
Bazel cepat. Bazel juga lambat, karena build cenderung tumbuh hingga batas yang dapat ditoleransi. Karena alasan ini, Bazel menyertakan profiler yang dapat
digunakan untuk membuat profil build dan Bazel itu sendiri. Class ini diterapkan dalam class yang
diberi nama 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.
Alat ini menghasilkan profil dalam format profiler Chrome; sebaiknya dilihat di Chrome. Model datanya adalah tumpukan tugas: seseorang dapat memulai tugas dan mengakhiri tugas, dan tugas tersebut seharusnya disusun bertingkat dengan rapi. Setiap thread Java mendapatkan stack tugasnya sendiri. TODO: Bagaimana cara kerjanya dengan tindakan dan gaya penerusan lanjutan?
Profiler dimulai dan dihentikan di BlazeRuntime.initProfiler()
dan
BlazeRuntime.afterCommand()
dan mencoba aktif selama
sempat sehingga kita dapat membuat profil semuanya. Untuk menambahkan sesuatu ke profil,
panggil Profiler.instance().profile()
. Fungsi 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
. Fitur 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 "pengujian integrasi" dan yang terakhir "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:
- Yang diimplementasikan menggunakan framework pengujian bash yang sangat rumit di
src/test/shell
- Yang diterapkan di Java. Ini diimplementasikan sebagai subclass
BuildIntegrationTestCase
BuildIntegrationTestCase
adalah framework pengujian integrasi pilihan karena
dilengkapi dengan baik untuk sebagian besar skenario pengujian. Sebagai framework Java, framework ini
menyediakan kemampuan debug dan integrasi yang lancar dengan banyak alat
pengembangan umum. Ada banyak contoh class BuildIntegrationTestCase
di
repositori Bazel.
Pengujian analisis diimplementasikan sebagai subclass BuildViewTestCase
. Ada
sistem file awal yang dapat Anda gunakan untuk menulis file BUILD
, lalu berbagai metode
helper dapat meminta target yang dikonfigurasi, mengubah konfigurasi, dan menyatakan
berbagai hal tentang hasil analisis.