Dokumen ini menjelaskan codebase dan struktur Bazel. Fitur ini ditujukan bagi orang yang bersedia berkontribusi pada Bazel, bukan untuk pengguna akhir.
Pengantar
Codebase Bazel sangat besar (~350 KLOC kode produksi dan ~260 KLOC kode pengujian) dan tidak ada yang memahami keseluruhan lanskapnya: semua orang sangat memahami lembahnya masing-masing, tetapi hanya sedikit yang tahu apa yang ada di balik bukit di setiap arah.
Agar orang yang berada di tengah perjalanan tidak tersesat di hutan gelap karena kehilangan jalur yang jelas, dokumen ini mencoba memberikan ringkasan codebase sehingga lebih mudah untuk mulai mengerjakannya.
Versi publik kode sumber Bazel ada di GitHub di github.com/bazelbuild/bazel. Ini bukan "sumber tepercaya"; ini berasal dari pohon 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 pull request GitHub biasa, dan diimpor secara manual oleh karyawan Google ke dalam hierarki sumber internal, lalu diekspor kembali ke GitHub.
Arsitektur klien/server
Sebagian besar Bazel berada dalam proses server yang tetap berada di RAM di antara build. Hal ini memungkinkan Bazel mempertahankan status di antara build.
Itulah sebabnya command line Bazel memiliki dua jenis opsi: startup dan 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 ruang kerja terkait (kumpulan pohon sumber yang dikenal sebagai "repositori") dan setiap ruang kerja biasanya memiliki satu instance server aktif. Hal ini dapat diatasi dengan menentukan dasar output kustom (lihat bagian "Tata letak direktori" untuk mengetahui informasi selengkapnya).
Bazel didistribusikan sebagai satu file yang dapat dieksekusi ELF yang juga merupakan file .zip yang valid.
Saat Anda mengetik bazel
, file ELF yang dapat dieksekusi di atas yang diimplementasikan di C++ ("klien") akan mendapatkan kontrol. Layanan ini menyiapkan proses server yang sesuai menggunakan
langkah-langkah berikut:
- Memeriksa apakah sudah mengekstrak dirinya sendiri. Jika tidak, hal itu akan dilakukan. Di sinilah penerapan server berasal.
- Memeriksa apakah ada instance server aktif yang berfungsi: instance tersebut berjalan, memiliki opsi startup yang tepat, dan menggunakan direktori ruang kerja yang tepat. Server ini
menemukan server yang sedang berjalan dengan melihat direktori
$OUTPUT_BASE/server
tempat terdapat file kunci dengan port yang digunakan server untuk memproses permintaan. - Jika perlu, menghentikan proses server lama
- Jika diperlukan, memulai proses server baru
Setelah proses server yang sesuai siap, perintah yang perlu dijalankan
dikomunikasikan ke server melalui antarmuka gRPC, lalu output Bazel dikirim kembali
ke terminal. Hanya satu perintah yang dapat berjalan pada waktu yang sama. Hal ini diimplementasikan menggunakan mekanisme penguncian yang rumit dengan bagian-bagian dalam C++ dan bagian-bagian dalam Java. Ada beberapa infrastruktur untuk menjalankan beberapa perintah secara paralel, karena ketidakmampuan untuk menjalankan bazel version
secara paralel dengan perintah lain agak memalukan. Hambatan utamanya adalah siklus proses BlazeModule
s
dan beberapa status di BlazeRuntime
.
Di akhir perintah, server Bazel mengirimkan kode keluar yang harus ditampilkan klien. Hal menarik yang perlu diperhatikan adalah penerapan bazel run
: tugas perintah ini adalah menjalankan sesuatu yang baru saja dibuat Bazel, tetapi perintah ini tidak dapat melakukannya dari proses server karena tidak memiliki terminal. Jadi, perintah ini memberi tahu klien biner mana yang harus exec()
dan dengan argumen apa.
Saat seseorang menekan Ctrl-C, klien menerjemahkannya menjadi panggilan Batal 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 serangkaian direktori yang agak rumit selama build. Deskripsi lengkap tersedia di Tata letak direktori output.
"Repo utama" adalah pohon sumber tempat Bazel dijalankan. Biasanya sesuai dengan sesuatu yang Anda keluarkan dari kontrol sumber. Root direktori ini dikenal sebagai "root ruang kerja".
Bazel menempatkan semua datanya di bawah "output user root". Nilai ini biasanya
$HOME/.cache/bazel/_bazel_${USER}
, tetapi dapat diganti menggunakan opsi peluncuran
--output_user_root
.
"Install base" adalah tempat Bazel diekstrak. Hal ini dilakukan secara otomatis
dan setiap versi Bazel mendapatkan subdirektori berdasarkan checksum-nya di bagian
dasar penginstalan. Setelan ini adalah $OUTPUT_USER_ROOT/install
secara default dan dapat diubah
menggunakan opsi command line --install_base
.
"Output dasar" adalah tempat Bazel instance yang terlampir ke ruang kerja tertentu menulis. Setiap dasar output memiliki paling banyak satu instance server Bazel yang berjalan setiap saat. Biasanya pada pukul $OUTPUT_USER_ROOT/<checksum of the path
to the workspace>
. Hal ini dapat diubah menggunakan opsi peluncuran --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. Terletak di
$OUTPUT_BASE/execroot
. Selama build, direktori kerja adalah$EXECROOT/<name of main repository>
. Kami berencana mengubahnya menjadi$EXECROOT
, meskipun ini adalah rencana jangka panjang karena perubahan ini sangat tidak kompatibel. - File yang dibuat selama build.
Proses menjalankan perintah
Setelah server Bazel mendapatkan kontrol dan diberi tahu tentang perintah yang perlu dijalankan, urutan peristiwa berikut akan terjadi:
BlazeCommandDispatcher
akan diberi tahu tentang permintaan baru. Perintah ini memutuskan 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 antipattern, akan lebih baik jika 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 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.
Perintah mendapatkan kontrol. Perintah yang paling menarik adalah perintah yang menjalankan build: build, test, run, coverage, dan sebagainya: fungsi ini diimplementasikan oleh
BuildTool
.Kumpulan pola target di command line diuraikan dan karakter pengganti seperti
//pkg:all
dan//pkg/...
diselesaikan. Hal ini diimplementasikan diAnalysisPhaseRunner.evaluateTargetPatterns()
dan diwujudkan di Skyframe sebagaiTargetPatternPhaseValue
.Fase pemuatan/analisis dijalankan untuk menghasilkan grafik tindakan (grafik asiklik terarah dari perintah yang perlu dijalankan untuk build).
Fase eksekusi dijalankan. Artinya, setiap tindakan yang diperlukan untuk membuat 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 "kelas 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 subclass dariFragmentOptions
dan akhirnya di-wrap ke dalam objekBuildOptions
. - 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 akan melakukan pemindaian include C++ atau tidak) dibaca
dalam fase eksekusi, tetapi hal itu selalu memerlukan penyiapan eksplisit karena
BuildConfiguration
tidak tersedia saat itu. Untuk mengetahui informasi selengkapnya, lihat bagian "Konfigurasi".
PERINGATAN: Kami suka berpura-pura bahwa instance OptionsBase
tidak dapat diubah dan menggunakannya seperti itu (seperti bagian dari SkyKeys
). Namun, hal ini tidak benar dan memodifikasinya adalah cara yang sangat baik untuk merusak Bazel dengan cara yang sulit di-debug. Sayangnya, membuat objek tersebut benar-benar tidak dapat diubah adalah upaya yang besar.
(Memodifikasi FragmentOptions
segera setelah konstruksi sebelum orang lain
mendapatkan kesempatan untuk menyimpan referensi ke FragmentOptions
dan sebelum equals()
atau hashCode()
dipanggil di FragmentOptions
tidak masalah.)
Bazel mempelajari class opsi dengan cara berikut:
- Beberapa di antaranya terhubung langsung ke Bazel (
CommonCommandOptions
) - Dari anotasi
@Command
pada setiap perintah Bazel - Dari
ConfiguredRuleClassProvider
(ini adalah opsi command line yang terkait dengan masing-masing bahasa pemrograman) - Aturan Starlark juga dapat menentukan opsinya sendiri (lihat di sini)
Setiap opsi (kecuali opsi yang ditentukan Starlark) adalah variabel anggota dari subkelas
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 menjadi tanggung jawab penerapan com.google.devtools.common.options.Converter
.
Hierarki sumber, seperti yang dilihat oleh Bazel
Bazel bergerak di bidang pembuatan software, yang dilakukan dengan membaca dan menafsirkan kode sumber. Keseluruhan kode sumber yang dioperasikan Bazel disebut "ruang kerja" dan disusun ke dalam repositori, paket, dan aturan.
Repositori
"Repositori" adalah source tree tempat developer bekerja; biasanya mewakili satu project. Pendahulu Bazel, Blaze, beroperasi di monorepo, yaitu satu pohon sumber yang berisi semua kode sumber yang digunakan untuk menjalankan build. Sebaliknya, Bazel mendukung project yang kode sumbernya mencakup beberapa repositori. Repositori tempat Bazel dipanggil disebut "repositori utama", sedangkan repositori lainnya disebut "repositori eksternal".
Repositori ditandai dengan file batas repo (MODULE.bazel
, REPO.bazel
, atau
dalam konteks lama, WORKSPACE
atau WORKSPACE.bazel
) di direktori root-nya. Repo
utama adalah pohon sumber tempat Anda memanggil Bazel. Repositori eksternal
ditentukan dengan berbagai cara; lihat ringkasan
dependensi eksternal untuk mengetahui informasi selengkapnya.
Kode repositori eksternal di-symlink atau didownload di
$OUTPUT_BASE/external
.
Saat menjalankan build, seluruh pohon sumber harus disatukan; hal ini dilakukan oleh SymlinkForest
, yang membuat link simbolik 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. Hal ini ditentukan oleh file yang disebut
BUILD
atau BUILD.bazel
. Jika keduanya ada, Bazel lebih memilih BUILD.bazel
; alasan
mengapa file BUILD
masih diterima adalah karena pendahulu Bazel, Blaze, menggunakan
nama file ini. Namun, ternyata segmen jalur ini sering digunakan, terutama di Windows, tempat nama file tidak peka huruf besar/kecil.
Setiap paket tidak saling memengaruhi: perubahan pada file BUILD
dari suatu paket tidak dapat menyebabkan perubahan pada paket lain. Penambahan atau penghapusan file BUILD
_dapat _mengubah paket lain, karena glob rekursif berhenti di batas paket
dan dengan demikian keberadaan file BUILD
menghentikan rekursi.
Evaluasi file BUILD
disebut "pemuatan paket". Hal ini diterapkan
di class PackageFactory
, berfungsi dengan memanggil interpreter Starlark dan
memerlukan pengetahuan tentang kumpulan class aturan yang tersedia. Hasil pemuatan
paket adalah objek Package
. Sebagian besar berupa peta dari string (nama target) ke target itu sendiri.
Sebagian besar kompleksitas selama pemuatan paket adalah globbing: Bazel tidak
memerlukan setiap file sumber dicantumkan secara eksplisit dan sebagai gantinya dapat menjalankan glob
(seperti glob(["**/*.java"])
). Tidak seperti shell, Bazel mendukung glob rekursif yang
turun ke subdirektori (tetapi tidak ke subpaket). Hal ini memerlukan akses ke sistem file dan karena hal itu bisa lambat, kami menerapkan berbagai trik untuk membuatnya berjalan secara paralel dan seefisien mungkin.
Globbing diterapkan di class berikut:
LegacyGlobber
, globber cepat yang tidak menyadari SkyframeSkyframeHybridGlobber
, versi yang menggunakan Skyframe dan kembali ke globber lama untuk menghindari "restart Skyframe" (dijelaskan di bawah)
Class Package
itu sendiri berisi beberapa anggota yang secara eksklusif digunakan untuk
mengurai paket "eksternal" (terkait dengan dependensi eksternal) dan yang tidak
masuk akal untuk paket sebenarnya. Hal ini merupakan
kesalahan desain karena objek yang mendeskripsikan paket reguler tidak boleh berisi
kolom yang mendeskripsikan hal lain. Ini mencakup:
- Pemetaan repositori
- Toolchain terdaftar
- Platform eksekusi yang terdaftar
Idealnya, akan ada pemisahan yang lebih besar antara mengurai paket "eksternal" dan mengurai paket reguler sehingga Package
tidak perlu memenuhi kebutuhan keduanya. Sayangnya, hal ini sulit dilakukan karena keduanya
sangat terkait.
Label, Target, dan Aturan
Paket terdiri dari target, yang memiliki jenis berikut:
- File: hal-hal yang merupakan input atau output build. Dalam istilah Bazel, kami menyebutnya artefak (dibahas di tempat lain). Tidak semua file yang dibuat selama build adalah target; output Bazel biasanya tidak memiliki label terkait.
- Aturan: ini menjelaskan langkah-langkah untuk mendapatkan output dari inputnya. Umumnya terkait dengan bahasa pemrograman (seperti
cc_library
,java_library
, ataupy_library
), tetapi ada beberapa 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
berada, dan name
adalah jalur
file (jika label merujuk ke file sumber) relatif terhadap direktori
paket. Saat merujuk ke target di command line, beberapa bagian label
dapat dihilangkan:
- Jika repositori tidak ada, label dianggap berada di repositori utama.
- Jika bagian paket dihilangkan (seperti
name
atau:name
), label dianggap berada dalam paket direktori kerja saat ini (jalur relatif yang berisi referensi tingkat atas (..) tidak diizinkan)
Jenis aturan (seperti "C++ library") disebut "class aturan". Class aturan dapat diimplementasikan di Starlark (fungsi rule()
) atau di Java (yang disebut "aturan native", jenis RuleClass
). Dalam jangka panjang, setiap aturan khusus bahasa akan diimplementasikan di Starlark, tetapi beberapa family 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 di ConfiguredRuleClassProvider
.
Class aturan berisi informasi seperti:
- Atributnya (seperti
srcs
,deps
): jenis, nilai default, batasan, dll. - Transisi dan aspek konfigurasi yang dilampirkan ke setiap atribut, jika ada
- Penerapan aturan
- Penyedia info transitif yang "biasanya" dibuat oleh aturan
Catatan terminologi: Dalam codebase, kita sering menggunakan "Rule" untuk merujuk pada target yang dibuat oleh class aturan. Namun, di Starlark dan dokumentasi yang ditampilkan kepada pengguna, "Rule" harus digunakan secara eksklusif untuk merujuk pada class aturan itu sendiri; target hanyalah "target". Perhatikan juga bahwa meskipun RuleClass
memiliki "class" dalam
namanya, tidak ada hubungan pewarisan Java antara class aturan dan target
jenis tersebut.
Skyframe
Framework evaluasi yang mendasari Bazel disebut Skyframe. Modelnya adalah semua yang perlu dibangun selama build disusun ke dalam grafik asiklik berarah dengan tepi yang mengarah dari setiap bagian data ke dependensinya, yaitu, bagian data lain 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 dapat dijangkau dari keduanya. Invarian ini hampir selalu berlaku, dan jika tidak berlaku (seperti untuk class opsi individual BuildOptions
, yang merupakan anggota BuildConfigurationValue
dan SkyKey
-nya), kami akan berupaya keras untuk tidak mengubahnya atau mengubahnya hanya dengan cara yang tidak dapat diamati dari luar.
Dari sini, dapat disimpulkan bahwa 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=deps
, yang akan menampilkan grafik, satu SkyValue
per baris. Sebaiknya lakukan untuk build kecil, karena ukurannya bisa menjadi cukup besar.
Skyframe ada di paket com.google.devtools.build.skyframe
. Paket com.google.devtools.build.lib.skyframe
yang namanya serupa 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, fungsi tersebut dapat meminta dependensi lain dari Skyframe dengan memanggil berbagai kelebihan beban SkyFunction.Environment.getValue()
. Hal ini memiliki
efek samping berupa pendaftaran dependensi tersebut ke dalam grafik internal Skyframe, sehingga
Skyframe akan mengetahui cara mengevaluasi ulang fungsi saat ada dependensi yang
berubah. Dengan kata lain, penyimpanan dalam cache dan komputasi inkremental Skyframe berfungsi pada
granularitas SkyFunction
dan SkyValue
.
Setiap kali SkyFunction
meminta dependensi yang tidak tersedia, getValue()
akan menampilkan null. Fungsi ini kemudian harus mengembalikan kontrol ke Skyframe dengan menampilkan null. Di lain waktu, Skyframe akan mengevaluasi dependensi yang tidak tersedia, lalu memulai ulang fungsi dari awal — hanya kali ini panggilan getValue()
akan berhasil dengan hasil non-null.
Akibatnya, semua komputasi yang dilakukan di dalam SkyFunction
sebelum dimulai ulang harus diulangi. Namun, hal ini tidak mencakup 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 beberapa bagian terpisah yang dihitung olehSkyFunction
yang berbeda, sehingga dapat dihitung dan di-cache secara terpisah. 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". Dengan SkyFunction yang kompleks, pengelolaan status di antara mulai ulang bisa menjadi rumit, jadiStateMachine
s diperkenalkan untuk pendekatan terstruktur terhadap konkurensi logis, termasuk hook untuk menangguhkan dan melanjutkan komputasi hierarkis dalamSkyFunction
. Contoh:DependencyResolver#computeDependencies
menggunakanStateMachine
dengangetState()
untuk menghitung kumpulan besar dependensi langsung dari target yang dikonfigurasi, yang jika tidak, dapat menyebabkan restart yang mahal.
Pada dasarnya, Bazel memerlukan jenis solusi ini karena ratusan ribu node Skyframe yang sedang berjalan adalah hal yang umum, dan dukungan Java untuk thread ringan tidak lebih unggul daripada implementasi StateMachine
pada tahun 2023.
Starlark
Starlark adalah bahasa khusus domain yang digunakan orang untuk mengonfigurasi dan memperluas Bazel. Python Mini dirancang sebagai subset Python terbatas yang memiliki lebih sedikit jenis, lebih banyak batasan pada alur kontrol, dan yang terpenting, jaminan imutabilitas yang kuat untuk memungkinkan pembacaan serentak. Bahasa ini tidak Turing-lengkap, yang membuat sebagian (tetapi tidak semua) pengguna enggan mencoba menyelesaikan tugas pemrograman umum dalam bahasa ini.
Starlark diimplementasikan dalam paket net.starlark.java
.
Library ini juga memiliki penerapan Go independen
di sini. Implementasi Java
yang digunakan di Bazel saat ini adalah interpreter.
Starlark digunakan dalam beberapa konteks, termasuk:
BUILD
file. Di sinilah target build baru ditentukan. Kode Starlark yang berjalan dalam konteks ini hanya memiliki akses ke konten fileBUILD
itu sendiri dan file.bzl
yang dimuat olehnya.- File
MODULE.bazel
. Di sinilah dependensi eksternal ditentukan. Kode Starlark yang berjalan dalam konteks ini hanya memiliki akses yang sangat terbatas ke beberapa direktif yang telah ditentukan sebelumnya. .bzl
file. Di sinilah aturan build baru, aturan repo, ekstensi modul ditentukan. Kode Starlark di sini dapat menentukan fungsi baru dan memuat dari file.bzl
lainnya.
Dialek yang tersedia untuk file BUILD
dan .bzl
sedikit berbeda
karena mengekspresikan hal yang berbeda. Daftar perbedaannya tersedia
di sini.
Informasi selengkapnya tentang Starlark tersedia di sini.
Fase pemuatan/analisis
Fase pemuatan/analisis adalah tempat Bazel menentukan tindakan apa yang diperlukan untuk membangun aturan tertentu. Unit dasarnya adalah "target yang dikonfigurasi", yang, cukup masuk akal, adalah pasangan (target, konfigurasi).
Bagian ini disebut "fase pemuatan/analisis" karena dapat dibagi menjadi dua bagian berbeda, yang sebelumnya diserialisasi, tetapi kini dapat tumpang-tindih dalam waktu:
- Memuat paket, yaitu mengubah file
BUILD
menjadi objekPackage
yang merepresentasikannya - Menganalisis target yang dikonfigurasi, yaitu menjalankan penerapan aturan untuk menghasilkan grafik tindakan
Setiap target yang dikonfigurasi dalam penutupan transitif dari target yang dikonfigurasi yang diminta di command line harus dianalisis dari bawah ke atas; yaitu, node daun terlebih dahulu, lalu hingga ke node yang ada 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. Objek ini disebut demikian karena menyediakan "penggabungan" 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 yang ada di target. Untuk aturan, hal ini mencakup atributnya, yang biasanya penting.
- Penerapan target yang dikonfigurasi. Untuk aturan, ini dapat berupa Starlark atau 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 mengalami 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" membangun 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, saat kode yang sama digunakan untuk alat yang dijalankan selama build dan untuk kode target, dan kita melakukan kompilasi silang atau saat kita mem-build aplikasi Android gemuk (yang berisi kode native untuk beberapa arsitektur CPU)
Secara konseptual, konfigurasi adalah instance BuildOptions
. Namun, dalam
praktiknya, BuildOptions
di-wrap oleh BuildConfiguration
yang menyediakan
berbagai fungsi tambahan. Proses ini berjalan dari bagian atas
grafik dependensi ke bagian bawah. Jika berubah, build perlu dianalisis ulang.
Hal ini menyebabkan anomali seperti harus menganalisis ulang seluruh build jika, misalnya, jumlah permintaan eksekusi pengujian berubah, meskipun hal itu hanya memengaruhi target pengujian (kami memiliki rencana untuk "memangkas" konfigurasi agar hal ini tidak terjadi, tetapi belum siap).
Jika implementasi aturan memerlukan sebagian konfigurasi, implementasi tersebut harus mendeklarasikannya dalam definisi menggunakan RuleClass.Builder.requiresConfigurationFragments()
. Hal ini dilakukan untuk menghindari kesalahan (seperti aturan Python yang menggunakan fragmen Java) dan
untuk memfasilitasi penghapusan konfigurasi sehingga jika opsi Python berubah, target C++
tidak perlu dianalisis ulang.
Konfigurasi aturan tidak harus sama dengan konfigurasi aturan "induk". Proses mengubah konfigurasi di tepi 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 tepi masuk ke target yang dikonfigurasi. Hal ini ditentukan dalam
RuleClass.Builder.cfg()
.
Class yang relevan adalah TransitionFactory
dan ConfigurationTransition
.
Transisi konfigurasi digunakan, misalnya:
- Untuk menyatakan bahwa dependensi tertentu digunakan selama build dan dengan demikian harus dibangun dalam arsitektur eksekusi
- Untuk menyatakan bahwa dependensi tertentu harus dibangun untuk beberapa arsitektur (seperti untuk kode native dalam APK Android gemuk)
Jika transisi konfigurasi menghasilkan beberapa konfigurasi, transisi tersebut disebut transisi pemisahan.
Transisi konfigurasi juga dapat diterapkan di Starlark (dokumentasi di sini)
Penyedia info transitif
Penyedia info transitif adalah cara (dan _satu-satunya _cara) agar target yang dikonfigurasi dapat mempelajari hal-hal tentang target lain yang dikonfigurasi yang menjadi dependensinya, dan satu-satunya cara untuk memberi tahu hal-hal tentang dirinya sendiri kepada target lain yang dikonfigurasi yang menjadi dependensinya. Alasan "transitif" ada dalam namanya adalah karena biasanya ini merupakan semacam penggabungan penutupan transitif dari target yang dikonfigurasi.
Umumnya ada korespondensi 1:1 antara penyedia info transitif Java dan Starlark (kecuali DefaultInfo
yang merupakan gabungan dari FileProvider
, FilesToRunProvider
, dan RunfilesProvider
karena API tersebut dianggap lebih mirip Starlark daripada transliterasi langsung dari Java).
Kunci mereka adalah salah satu hal berikut:
- Objek Class Java. Fungsi ini hanya tersedia untuk penyedia yang tidak dapat diakses dari Starlark. Penyedia ini adalah subclass dari
TransitiveInfoProvider
. - String. Ini adalah fitur lama dan sangat tidak disarankan karena rentan terhadap
konflik 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 direpresentasikan oleh instanceProvider.Key
di Java.
Penyedia baru yang diimplementasikan di Java harus diimplementasikan menggunakan BuiltinProvider
.
NativeProvider
tidak digunakan lagi (kami belum sempat menghapusnya) dan subkelas
TransitiveInfoProvider
tidak dapat diakses dari Starlark.
Target yang dikonfigurasi
Target yang dikonfigurasi diterapkan sebagai RuleConfiguredTargetFactory
. Ada subkelas 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. Terdiri dari hal-hal berikut:
filesToBuild
mereka, konsep samar "kumpulan file yang diwakili oleh aturan ini". Ini adalah file yang dibuat saat target yang dikonfigurasi ada di command line atau di srcs genrule.- File yang dijalankan, reguler dan data.
- Grup output mereka. Ini adalah berbagai "kumpulan file lainnya" yang dapat dibuat oleh aturan. File tersebut dapat diakses menggunakan atribut output_group dari
aturan filegroup di BUILD dan menggunakan penyedia
OutputGroupInfo
di Java.
Runfiles
Beberapa biner memerlukan file data untuk dijalankan. Contoh yang jelas adalah pengujian yang memerlukan file input. Hal ini diwakili di Bazel oleh konsep "runfiles". "Pohon runfiles" adalah pohon direktori file data untuk biner tertentu. Direktori ini dibuat dalam sistem file sebagai pohon symlink dengan symlink individual yang mengarah ke file dalam pohon sumber atau output.
Kumpulan file yang dapat dijalankan direpresentasikan sebagai instance Runfiles
. Secara konseptual, ini adalah
peta dari jalur file di pohon runfile ke instance Artifact
yang
mewakilinya. Prosesnya sedikit lebih rumit daripada satu Map
untuk dua alasan:
- Biasanya, jalur runfile suatu file sama dengan execpath-nya. Kita menggunakan ini untuk menghemat RAM.
- Ada berbagai jenis entri lama di pohon runfile, yang juga perlu direpresentasikan.
Runfile dikumpulkan menggunakan RunfilesProvider
: instance class ini merepresentasikan runfile yang dibutuhkan oleh target yang dikonfigurasi (seperti library) dan penutupan transitifnya, dan dikumpulkan seperti set bertingkat (sebenarnya, runfile diimplementasikan menggunakan set bertingkat di bawahnya): setiap target menggabungkan runfile dependensinya, menambahkan beberapa runfile miliknya, lalu mengirimkan set yang dihasilkan ke atas dalam grafik dependensi. Instance RunfilesProvider
berisi dua instance Runfiles
, satu untuk saat aturan bergantung melalui atribut "data" dan satu untuk setiap jenis dependensi masuk lainnya. Hal ini karena target
terkadang menampilkan file yang berbeda saat bergantung melalui atribut data
daripada jika tidak. 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 dibangun (tidak seperti Runfiles
, yang hanya berupa pemetaan). Hal ini memerlukan komponen tambahan berikut:
- Manifes runfile input. Ini adalah deskripsi berseri dari hierarki file yang dapat dijalankan. File ini digunakan sebagai proxy untuk konten hierarki runfile dan Bazel mengasumsikan bahwa hierarki runfile berubah jika dan hanya jika konten manifes berubah.
- Manifes runfile output. Ini digunakan oleh library runtime yang menangani hierarki runfile, terutama di Windows, yang terkadang tidak mendukung link simbolik.
- Argumen command line untuk menjalankan biner yang runfilenya diwakili oleh objek
RunfilesSupport
.
Aspek
Aspek adalah cara untuk "menyebarkan komputasi ke bawah grafik dependensi". Deskripsinya untuk pengguna Bazel dapat dilihat di sini. Contoh motivasi yang baik adalah buffer protokol: aturan proto_library
tidak boleh mengetahui bahasa tertentu, tetapi membangun implementasi pesan buffer protokol ("unit dasar" buffer protokol) dalam bahasa pemrograman apa pun harus digabungkan dengan aturan proto_library
sehingga jika dua target dalam bahasa yang sama bergantung pada buffer protokol yang sama, buffer tersebut hanya dibangun satu kali.
Sama seperti target yang dikonfigurasi, target tersebut direpresentasikan di Skyframe sebagai SkyValue
dan cara pembuatannya sangat mirip dengan cara target yang dikonfigurasi
dibuat: target tersebut memiliki class factory yang disebut ConfiguredAspectFactory
yang memiliki
akses ke RuleContext
, tetapi tidak seperti factory target yang dikonfigurasi, target tersebut juga mengetahui
target yang dikonfigurasi yang dilampirkan padanya dan penyedianya.
Kumpulan aspek yang diteruskan ke bawah grafik dependensi ditentukan untuk setiap
atribut menggunakan fungsi Attribute.Builder.aspects()
. Ada beberapa class yang namanya membingungkan yang berpartisipasi dalam proses ini:
AspectClass
adalah penerapan aspek. Objek ini dapat berupa di Java (dalam hal ini, objek tersebut adalah subclass) atau di Starlark (dalam hal ini, objek tersebut adalah instanceStarlarkAspectClass
). Objek ini analog denganRuleConfiguredTargetFactory
.AspectDefinition
adalah definisi aspek; ini mencakup penyedia yang diperlukan, penyedia yang disediakan, dan berisi referensi ke implementasinya, seperti instanceAspectClass
yang sesuai. Hal ini mirip denganRuleClass
.AspectParameters
adalah cara untuk memarameterisasi aspek yang diteruskan ke bawah grafik dependensi. Saat ini berupa peta string ke string. Contoh yang baik tentang kegunaannya adalah buffer protokol: jika suatu bahasa memiliki beberapa API, informasi mengenai API mana yang harus dibuat untuk buffer protokol harus diteruskan ke bawah grafik dependensi.Aspect
merepresentasikan semua data yang diperlukan untuk menghitung aspek yang diturunkan dalam grafik dependensi. Terdiri dari class aspek, definisi, dan parameternya.RuleAspect
adalah fungsi yang menentukan aspek mana yang harus dipropagasi 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 dicatat 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 tempat kode dibangun. Arsitektur ini disebut sebagai platform dalam istilah Bazel (dokumentasi lengkap di sini)
Platform dijelaskan oleh pemetaan key-value 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, seseorang mungkin perlu menggunakan compiler yang berbeda; misalnya, toolchain C++ tertentu dapat berjalan di OS tertentu dan dapat menargetkan OS lain. Bazel harus menentukan compiler C++ yang digunakan berdasarkan eksekusi yang ditetapkan dan platform target (dokumentasi untuk toolchain di sini).
Untuk melakukannya, toolchain diberi anotasi dengan serangkaian batasan platform target dan eksekusi 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 toolchain (seperti C++ atau Java) (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 untuk melakukan penyelesaian toolchain dan aturan khusus bahasa
*_toolchain()
berisi lebih banyak informasi daripada itu, sehingga memerlukan lebih banyak
waktu untuk dimuat.
Platform eksekusi ditentukan dengan salah satu cara berikut:
- Di file MODULE.bazel 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 kami ingin mendukung beberapa platform target pada akhirnya, tetapi belum diterapkan.
Kumpulan toolchain yang akan digunakan untuk target yang dikonfigurasi ditentukan oleh
ToolchainResolutionFunction
. Hal ini merupakan fungsi dari:
- Kumpulan toolchain terdaftar (dalam file MODULE.bazel dan konfigurasi)
- Platform eksekusi dan target yang diinginkan (dalam konfigurasi)
- Kumpulan jenis toolchain yang diperlukan oleh target yang dikonfigurasi (dalam
UnloadedToolchainContextKey)
- Kumpulan batasan platform eksekusi dari target yang dikonfigurasi (atribut
exec_compatible_with
) dan konfigurasi (--experimental_add_exec_constraints_to_targets
), diUnloadedToolchainContextKey
Hasilnya adalah UnloadedToolchainContext
, yang pada dasarnya adalah peta dari
jenis toolchain (direpresentasikan sebagai instance ToolchainTypeInfo
) ke label
toolchain yang dipilih. Disebut "tidak dimuat" karena tidak berisi toolchain itu sendiri, hanya labelnya.
Kemudian, toolchain benar-benar dimuat menggunakan ResolvedToolchainContext.load()
dan digunakan oleh penerapan target yang dikonfigurasi yang memintanya.
Kami juga memiliki sistem lama yang mengandalkan satu konfigurasi "host" dan konfigurasi target yang diwakili oleh berbagai tanda konfigurasi, seperti --cpu
. Kami secara bertahap beralih 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 ada di PlatformMappingFunction
dan menggunakan "bahasa kecil" non-Starlark.
Batasan
Terkadang orang ingin menetapkan target agar kompatibel hanya dengan beberapa platform. Bazel (sayangnya) 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 segera 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 banyak digunakan.
Semua aturan build dapat menyatakan "lingkungan" yang dapat dibangun, dengan
"lingkungan" adalah instance aturan environment()
.
Ada berbagai cara lingkungan yang didukung dapat ditentukan untuk suatu aturan:
- Melalui atribut
restricted_to=
. Ini adalah bentuk spesifikasi yang paling langsung; mendeklarasikan kumpulan lingkungan persis yang didukung aturan. - 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 termasuk dalam grup rekan yang terkait secara tematis (seperti "arsitektur CPU", "versi JDK", atau "sistem operasi seluler"). Definisi grup lingkungan mencakup lingkungan mana yang harus didukung oleh "default" jika tidak ditentukan lain oleh atributrestricted_to=
/environment()
. Aturan tanpa atribut tersebut akan mewarisi semua nilai default. - Melalui default class aturan. Tindakan ini akan menggantikan default global untuk semua
instance class aturan tertentu. Hal ini dapat digunakan, misalnya, untuk membuat semua aturan
*_test
dapat diuji tanpa setiap instance harus menyatakan 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
bernama sama. Hal ini dilakukan untuk menghindari dependensi siklik yang akan muncul karena setiap
lingkungan perlu mendeklarasikan grup lingkungan tempatnya berada dan setiap
grup lingkungan perlu mendeklarasikan lingkungan defaultnya.
Build dapat dibatasi ke lingkungan tertentu dengan opsi command line --target_environment
.
Implementasi pemeriksaan batasan ada di
RuleContextConstraintSemantics
dan TopLevelConstraintSemantics
.
Batasan platform
Cara "resmi" saat ini untuk mendeskripsikan kompatibilitas platform target adalah dengan menggunakan batasan yang sama yang digunakan untuk mendeskripsikan toolchain dan platform. Fitur ini diimplementasikan dalam permintaan pull #10945.
Visibilitas
Jika Anda mengerjakan codebase besar dengan banyak developer (seperti di Google), Anda harus berhati-hati agar orang lain tidak 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 membatasi target mana yang dapat bergantung pada target tertentu menggunakan atribut visibilitas. Atribut ini sedikit istimewa karena, meskipun menyimpan daftar label, label ini dapat mengenkode pola pada nama paket, bukan penunjuk ke target tertentu. (Ya, ini adalah kekurangan desain.)
Hal ini diterapkan di tempat berikut:
- Antarmuka
RuleVisibility
merepresentasikan deklarasi visibilitas. Nilai ini dapat berupa konstanta (sepenuhnya publik atau sepenuhnya pribadi) atau daftar label. - Label dapat merujuk ke grup paket (daftar paket yang telah ditentukan sebelumnya), ke
paket secara langsung (
//pkg:__pkg__
) atau subpohon paket (//pkg:__subpackages__
). Hal ini berbeda dengan sintaksis command line, yang menggunakan//pkg:*
atau//pkg/...
. - Grup paket diimplementasikan sebagai targetnya sendiri (
PackageGroup
) dan target yang dikonfigurasi (PackageGroupConfiguredTarget
). Kita mungkin dapat menggantinya dengan aturan sederhana jika mau. 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 lain yang tidak diklasifikasikan. - Pemeriksaan sebenarnya dilakukan di
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
Kumpulan bertingkat
Sering kali, target yang dikonfigurasi menggabungkan sekumpulan file dari dependensinya, menambahkan file-nya sendiri, dan membungkus set gabungan 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 merepresentasikan penutupan transitif
cc_library
- Kumpulan file .jar yang harus ada di classpath agar aturan Java dapat dikompilasi atau dijalankan
- Kumpulan file Python dalam penutupan transitif aturan Python
Jika kita melakukannya dengan cara sederhana menggunakan, misalnya, List
atau Set
, kita akan mendapatkan penggunaan memori kuadratik: jika ada rangkaian N aturan dan setiap aturan menambahkan file, kita akan memiliki 1+2+...+N anggota koleksi.
Untuk mengatasi masalah ini, kami menemukan konsep
NestedSet
. Struktur data ini terdiri dari instance NestedSet
lain dan beberapa anggotanya sendiri, sehingga membentuk grafik asiklik berarah
dari set. Objek tidak dapat diubah dan anggotanya dapat diulang. Kita menentukan
beberapa urutan iterasi (NestedSet.Order
): preorder, postorder, topologi
(node selalu muncul setelah ancestor-nya) dan "tidak masalah, tetapi harus sama setiap kali".
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 ditampilkan sebagai instance class Action
dan file ditampilkan sebagai instance class Artifact
. Tindakan ini disusun dalam grafik asiklik berarah bipartit yang disebut
"grafik tindakan".
Artefak terdiri dari dua jenis: artefak sumber (yang tersedia sebelum Bazel mulai dieksekusi) dan artefak turunan (yang perlu dibuat). Artefak turunan sendiri dapat berupa beberapa jenis:
- Artefak reguler. File ini diperiksa kebaruannya dengan menghitung checksum-nya, dengan mtime sebagai pintasan; kita tidak menghitung checksum file jika ctime-nya tidak berubah.
- Artefak symlink yang belum terselesaikan. File ini diperiksa kebaruannya dengan memanggil readlink(). Tidak seperti artefak biasa, file ini dapat berupa symlink yang tidak terhubung. Biasanya digunakan dalam kasus di mana seseorang mengemas beberapa file ke dalam arsip tertentu.
- Artefak hierarki. Ini bukan file tunggal, tetapi struktur direktori. File tersebut diperiksa untuk memastikan kebaruannya dengan memeriksa kumpulan file di dalamnya dan kontennya. Objek ini direpresentasikan sebagai
TreeArtifact
. - Artefak metadata konstan. Perubahan pada artefak ini tidak memicu pembangunan ulang. Ini digunakan secara eksklusif untuk informasi stempel build: kita tidak ingin melakukan pembangunan ulang hanya karena waktu saat ini berubah.
Tidak ada alasan mendasar mengapa artefak sumber tidak dapat berupa artefak hierarki atau
artefak symlink yang belum diselesaikan, hanya saja kami belum menerapkannya (meskipun
seharusnya sudah -- mereferensikan direktori sumber dalam file BUILD
adalah salah satu
dari beberapa masalah ketidakakuratan yang sudah lama diketahui di Bazel; kami memiliki
implementasi yang berfungsi dan diaktifkan oleh properti JVM
BAZEL_TRACK_SOURCE_DIRECTORIES=1
)
Tindakan paling baik dipahami sebagai perintah yang perlu dijalankan, lingkungan yang dibutuhkan, dan serangkaian 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 menjelaskan lingkungan (seperti platform) yang diperlukan untuk menjalankannya \
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, seharusnya tidak menjadi
class terpisah), meskipun Java dan C++ memiliki jenis tindakan sendiri
(JavaCompileAction
, CppCompileAction
, dan CppLinkAction
).
Pada akhirnya, kita ingin memindahkan semuanya ke SpawnAction
; JavaCompileAction
sudah cukup dekat, tetapi C++ sedikit berbeda karena penguraian file .d dan pemindaian include.
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()
dan 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 bagi tindakan yang membuatnya - Set bertingkat memiliki kunci Skyframe 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 (tetapi meskipun demikian, aturan dalam paket yang sama dapat berkonflik), tetapi aturan yang diterapkan di Java dapat menempatkan artefak turunan di mana saja.
Hal ini dianggap sebagai kesalahan fitur, tetapi menghilangkannya sangat sulit karena menghasilkan penghematan waktu eksekusi yang signifikan ketika, misalnya, file sumber perlu diproses dengan cara tertentu dan file tersebut dirujuk oleh beberapa aturan (handwave-handwave). Hal ini memerlukan biaya RAM: setiap instance tindakan bersama harus disimpan dalam memori secara terpisah.
Jika dua tindakan menghasilkan file output yang sama, keduanya harus sama persis:
memiliki input yang sama, output yang sama, dan menjalankan command line yang sama. Relasi
ekuivalen ini 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 "global" dari
build.
Fase eksekusi
Pada tahap ini, Bazel benar-benar mulai menjalankan tindakan build, seperti perintah yang menghasilkan output.
Hal pertama yang dilakukan Bazel setelah fase analisis adalah menentukan Artefak yang perlu dibangun. 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 menyatakan "jika target ini ada di command
line, bangun artefak ini".
Langkah berikutnya adalah membuat root eksekusi. Karena Bazel memiliki opsi untuk membaca paket sumber dari lokasi yang berbeda dalam sistem file (--package_path
), Bazel perlu menyediakan tindakan yang dieksekusi secara lokal dengan pohon sumber lengkap. Hal ini ditangani oleh class SymlinkForest
dan berfungsi dengan mencatat setiap target yang digunakan dalam fase analisis dan membangun satu pohon direktori yang membuat link simbolik 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:
- Mengubah command line tindakan saat paket dipindahkan dari satu entri jalur paket ke entri lainnya (dulu sering terjadi)
- Hal ini menghasilkan baris perintah yang berbeda jika tindakan dijalankan dari jarak jauh daripada jika dijalankan secara lokal
- Hal ini memerlukan transformasi command line khusus untuk alat yang digunakan (pertimbangkan perbedaan antara jalur class Java dan jalur include C++)
- Mengubah command line tindakan akan membatalkan entri cache tindakan
--package_path
perlahan dan pasti tidak digunakan lagi
Kemudian, Bazel mulai melintasi grafik tindakan (grafik terarah bipartit
yang terdiri dari tindakan serta artefak input dan outputnya) dan menjalankan tindakan.
Eksekusi setiap tindakan diwakili oleh instance class SkyValue
ActionExecutionValue
.
Karena menjalankan tindakan itu mahal, kami memiliki beberapa lapisan caching yang dapat diakses di belakang Skyframe:
ActionExecutionFunction.stateMap
berisi data untuk membuat mulai ulang SkyframeActionExecutionFunction
menjadi murah- Cache tindakan lokal berisi data tentang status sistem file
- Sistem eksekusi jarak jauh biasanya juga berisi cache-nya sendiri
Cache tindakan lokal
Cache ini adalah lapisan lain yang berada di belakang Skyframe; meskipun tindakan dieksekusi ulang di Skyframe, tindakan tersebut masih dapat menjadi hit di cache tindakan lokal. Objek ini merepresentasikan status sistem file lokal dan diserialisasi ke disk, yang berarti bahwa saat server Bazel baru dimulai, server tersebut dapat memperoleh hit cache tindakan lokal meskipun grafik Skyframe kosong.
Cache ini diperiksa untuk mengetahui hit menggunakan metode
ActionCacheChecker.getTokenIfNeedToExecute()
.
Berbeda dengan namanya, ini adalah peta dari jalur artefak turunan ke tindakan yang memancarkannya. Tindakan tersebut dijelaskan sebagai:
- Kumpulan file input dan outputnya serta checksum-nya
- "Kunci tindakan", yang biasanya berupa command line yang dieksekusi, tetapi
secara umum, merepresentasikan semua yang tidak dicatat oleh checksum
file input (seperti untuk
FileWriteAction
, ini adalah checksum data yang ditulis)
Ada juga "cache tindakan dari atas ke bawah" yang sangat eksperimental dan masih dalam pengembangan, yang menggunakan hash transitif untuk menghindari akses ke cache berkali-kali.
Penemuan input dan penghapusan input
Beberapa tindakan lebih rumit daripada hanya memiliki serangkaian 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 menebak 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 praprosesor C lengkap) Opsi ini saat ini terhubung secara tetap ke "false" di Bazel dan hanya digunakan di Google. - Tindakan dapat menyadari bahwa beberapa file tidak digunakan selama eksekusinya. Di C++, ini disebut "file .d": compiler memberi tahu file header mana yang digunakan setelahnya, 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.
Tindakan ini diterapkan menggunakan metode pada Tindakan:
Action.discoverInputs()
dipanggil. Metode ini harus menampilkan set bertingkat Artefak yang ditentukan sebagai wajib. Ini harus berupa artefak sumber sehingga tidak ada tepi dependensi dalam grafik tindakan yang tidak memiliki padanan dalam grafik target yang dikonfigurasi.- Tindakan 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 mengakibatkan build inkremental yang salah jika input yang digunakan dilaporkan sebagai tidak digunakan.
Saat cache tindakan menampilkan kecocokan pada instance Tindakan baru (seperti yang dibuat
setelah server dimulai ulang), Bazel akan memanggil updateInputs()
itu sendiri sehingga set
input mencerminkan hasil penemuan dan penghapusan input yang dilakukan sebelumnya.
Tindakan Starlark dapat menggunakan fasilitas untuk mendeklarasikan beberapa input sebagai tidak digunakan
menggunakan argumen unused_inputs_list=
dari
ctx.actions.run()
.
Berbagai cara untuk menjalankan tindakan: Strategi/ActionContexts
Beberapa tindakan dapat dijalankan dengan cara yang berbeda. Misalnya, command line dapat dieksekusi secara lokal, secara lokal tetapi di berbagai jenis sandbox, atau dari jarak jauh. Konsep yang mewujudkan hal ini disebut ActionContext
(atau Strategy
, karena kita hanya berhasil menyelesaikan setengah dari penggantian nama...)
Siklus proses konteks tindakan adalah sebagai berikut:
- Saat fase eksekusi dimulai, instance
BlazeModule
ditanya konteks tindakan apa yang dimilikinya. 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 dipilih dari yang tersedia dan diteruskan ke
ActionExecutionContext
danBlazeExecutor
. - Konteks permintaan tindakan menggunakan
ActionExecutionContext.getContext()
danBlazeExecutor.getStrategy()
(sebenarnya hanya ada satu cara untuk melakukannya…)
Strategi dapat memanggil strategi lain secara gratis untuk melakukan tugasnya; hal ini digunakan, misalnya, dalam strategi dinamis yang memulai tindakan baik secara lokal maupun jarak jauh, lalu menggunakan tindakan yang selesai lebih dulu.
Salah satu strategi penting adalah yang menerapkan proses pekerja persisten (WorkerSpawnStrategy
). Idenya adalah bahwa beberapa alat memiliki waktu mulai yang lama dan oleh karena itu harus digunakan kembali di antara tindakan, bukan memulai yang baru untuk setiap tindakan (Hal ini menimbulkan potensi masalah kebenaran, karena Bazel mengandalkan janji proses pekerja bahwa proses tersebut tidak membawa status yang dapat diamati di antara setiap permintaan)
Jika alat berubah, proses pekerja perlu dimulai ulang. Apakah pekerja dapat digunakan kembali ditentukan dengan menghitung checksum untuk alat yang digunakan menggunakan
WorkerFilesHash
. Hal ini bergantung pada pengetahuan tentang input tindakan mana yang mewakili bagian dari alat dan input mana yang mewakili input; hal ini ditentukan oleh pembuat Tindakan: Spawn.getToolFiles()
dan file yang dapat dieksekusi dari 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 seluk-beluk menjalankan tindakan secara lokal tersedia di sini.
Pengelola resource lokal
Bazel dapat menjalankan banyak tindakan secara paralel. Jumlah tindakan lokal yang harus dijalankan secara paralel berbeda-beda dari satu tindakan ke tindakan lainnya: makin banyak resource yang diperlukan suatu tindakan, makin sedikit instance yang harus dijalankan secara bersamaan untuk menghindari kelebihan beban pada komputer lokal.
Hal ini diimplementasikan di class ResourceManager
: setiap tindakan harus
diberi anotasi dengan perkiraan resource lokal yang diperlukan dalam bentuk
instance ResourceSet
(CPU dan RAM). Kemudian, saat konteks tindakan melakukan sesuatu
yang memerlukan resource lokal, konteks tindakan akan memanggil ResourceManager.acquireResources()
dan diblokir hingga resource yang diperlukan tersedia.
Deskripsi yang lebih mendetail tentang pengelolaan resource lokal tersedia di sini.
Struktur direktori output
Setiap tindakan memerlukan tempat terpisah di direktori output tempat tindakan tersebut menempatkan outputnya. Lokasi artefak turunan biasanya sebagai berikut:
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
Bagaimana nama direktori yang terkait dengan konfigurasi tertentu ditentukan? Ada dua properti yang diinginkan dan saling 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, misalnya, mengenai command line suatu tindakan yang menghasilkan file output yang sama, Bazel tidak tahu tindakan mana yang harus dipilih (konflik tindakan)
- Jika dua konfigurasi merepresentasikan hal yang "kira-kira" sama, keduanya harus memiliki nama yang sama sehingga tindakan yang dijalankan di salah satunya dapat digunakan kembali untuk yang lain jika baris perintahnya cocok: misalnya, perubahan pada opsi baris perintah ke compiler Java tidak boleh menyebabkan tindakan kompilasi C++ dijalankan ulang.
Sejauh ini, kami belum menemukan cara yang berprinsip untuk memecahkan masalah ini, yang memiliki kesamaan dengan masalah penghapusan konfigurasi. Diskusi lebih lanjut tentang opsi tersedia di sini. Area utama yang bermasalah adalah aturan Starlark (yang penulisnya biasanya tidak terlalu memahami Bazel) dan aspek, yang menambahkan dimensi lain ke ruang lingkup 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 set transisi konfigurasi Starlark ditambahkan sehingga pengguna tidak dapat menyebabkan konflik tindakan. Ini jauh dari sempurna. Hal ini diterapkan di
OutputDirectories.buildMnemonic()
dan mengandalkan 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 menghilangkan ketidakstabilan atau mengumpulkan data waktu)
- Membagi pengujian (membagi kasus pengujian dalam pengujian yang sama di beberapa proses untuk kecepatan)
- Menjalankan kembali pengujian tidak stabil
- Mengelompokkan pengujian ke dalam test suite
Pengujian adalah target yang dikonfigurasi secara rutin yang memiliki TestProvider, yang menjelaskan cara pengujian harus dijalankan:
- Artefak yang hasil build-nya menyebabkan pengujian dijalankan. Ini adalah file "status cache" yang berisi pesan
TestResultData
yang diserialisasi - Jumlah pengujian yang harus dijalankan
- Jumlah shard yang harus dibagi untuk pengujian
- Beberapa parameter tentang cara pengujian harus dijalankan (seperti waktu tunggu pengujian)
Menentukan pengujian yang akan dijalankan
Menentukan pengujian mana yang dijalankan adalah proses yang rumit.
Pertama, selama penguraian pola target, rangkaian pengujian diperluas secara rekursif. Perluasan diimplementasikan di TestsForTargetPatternFunction
. Hal yang agak
mengejutkan adalah jika rangkaian pengujian tidak menyatakan adanya pengujian, rangkaian pengujian tersebut merujuk ke
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 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 TargetPatternPhaseValue.getTestsToRunLabels()
. Alasan mengapa atribut aturan yang dapat difilter tidak dapat dikonfigurasi adalah karena hal ini terjadi sebelum fase analisis, sehingga konfigurasi tidak tersedia.
Kemudian, data ini diproses lebih lanjut di BuildView.createResult()
: target yang analisisnya gagal akan difilter dan pengujian dibagi menjadi pengujian eksklusif dan non-eksklusif. Kemudian, dimasukkan ke AnalysisResult
, yang merupakan cara
ExecutionTool
mengetahui pengujian mana yang akan dijalankan.
Untuk memberikan transparansi pada proses yang rumit ini, operator kueri tests()
(diimplementasikan di TestsFunction
) tersedia untuk mengetahui pengujian mana yang dijalankan saat target tertentu ditentukan di command line. Sayangnya, ini adalah penerapan ulang, sehingga mungkin menyimpang dari di atas dalam beberapa cara yang tidak terlalu terlihat.
Menjalankan pengujian
Cara pengujian dilakukan adalah dengan meminta artefak status cache. Kemudian, hal ini akan
menghasilkan eksekusi TestRunnerAction
, yang pada akhirnya memanggil
TestActionContext
yang dipilih oleh opsi command line --test_strategy
yang
menjalankan pengujian dengan cara yang diminta.
Pengujian dijalankan sesuai dengan protokol rumit yang menggunakan variabel lingkungan untuk memberi tahu pengujian apa yang diharapkan dari pengujian tersebut. Deskripsi mendetail tentang apa yang diharapkan Bazel dari pengujian dan apa yang dapat diharapkan pengujian dari Bazel tersedia di sini. Paling sederhana, kode keluar 0 berarti berhasil, kode lainnya berarti gagal.
Selain file status cache, setiap proses pengujian memancarkan sejumlah file lainnya. File 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 dalam shard pengujiantest.log
, output konsol pengujian. stdout dan stderr tidak dipisahkan.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 pengujian yang tidak dapat terjadi selama membangun target reguler: eksekusi pengujian eksklusif dan streaming output.
Beberapa pengujian perlu dijalankan dalam mode eksklusif, misalnya tidak secara paralel dengan pengujian lain. Hal ini dapat dilakukan 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 biasa, yang output terminalnya di-dump saat tindakan
selesai, pengguna dapat meminta output pengujian untuk di-streaming sehingga mereka
dapat mengetahui progres pengujian yang berjalan lama. Hal ini ditentukan oleh opsi command line
--test_output=streamed
dan menyiratkan eksekusi pengujian eksklusif
sehingga output pengujian yang berbeda tidak diselingi.
Hal ini diimplementasikan dalam class StreamedTestOutput
yang dinamai dengan tepat dan berfungsi dengan
melakukan polling perubahan pada file test.log
dari pengujian yang dimaksud dan membuang byte baru
ke terminal tempat aturan Bazel berada.
Hasil pengujian yang dijalankan tersedia di bus peristiwa dengan mengamati
berbagai peristiwa (seperti TestAttempt
, TestResult
, atau TestingCompleteEvent
).
Hasil tersebut di-dump ke Build Event Protocol dan ditampilkan ke konsol
oleh AggregatingTestListener
.
Koleksi cakupan
Cakupan dilaporkan oleh pengujian dalam format LCOV di file
bazel-testlogs/$PACKAGE/$TARGET/coverage.dat
.
Untuk mengumpulkan cakupan, setiap eksekusi pengujian di-wrap 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 itu sendiri dapat menjalankan beberapa subproses dan terdiri dari bagian yang ditulis dalam beberapa bahasa pemrograman yang berbeda (dengan runtime pengumpulan cakupan yang terpisah). Skrip wrapper bertanggung jawab untuk mengonversi file yang dihasilkan ke format LCOV jika perlu, dan menggabungkannya ke dalam satu file.
Penyisipan collect_coverage.sh
dilakukan oleh strategi pengujian dan
memerlukan collect_coverage.sh
berada di input pengujian. Hal ini dilakukan oleh atribut implisit :coverage_support
yang diselesaikan 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. Ini adalah cakupan library,
biner, atau pengujian jika tidak ada kode di dalamnya yang dijalankan. Masalah yang dipecahkannya adalah jika Anda
ingin menghitung cakupan pengujian untuk biner, tidak cukup hanya menggabungkan
cakupan semua pengujian 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 kita kumpulkan cakupannya tanpa baris
yang tercakup. File cakupan dasar pengukuran default untuk target berada di
bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
, tetapi aturan
dianjurkan untuk membuat file cakupan dasar pengukuran sendiri dengan konten yang lebih bermakna
daripada hanya nama file sumber.
Kami melacak dua grup file untuk pengumpulan cakupan untuk setiap aturan: set file yang diinstrumentasi dan set file metadata instrumentasi.
Kumpulan file yang diinstrumentasi hanyalah itu, kumpulan file yang akan diinstrumentasi. Untuk runtime cakupan online, ini dapat digunakan saat runtime untuk memutuskan file mana yang akan diinstrumentasi. Data ini juga digunakan untuk menerapkan cakupan dasar.
Kumpulan file metadata instrumentasi adalah kumpulan file tambahan yang diperlukan pengujian untuk membuat file LCOV yang diperlukan Bazel darinya. Dalam praktiknya, ini terdiri dari file khusus runtime; misalnya, gcc memancarkan file .gcno selama kompilasi. Ini ditambahkan ke set input tindakan pengujian jika mode cakupan diaktifkan.
Apakah cakupan dikumpulkan atau tidak disimpan di
BuildConfiguration
. Hal ini berguna karena merupakan cara mudah untuk mengubah tindakan pengujian dan grafik tindakan, bergantung pada bit ini, tetapi juga berarti bahwa jika bit ini dibalik, semua target perlu dianalisis ulang (beberapa bahasa, seperti C++, memerlukan opsi compiler yang berbeda untuk memancarkan kode yang dapat mengumpulkan cakupan, yang mengurangi masalah ini, karena analisis ulang diperlukan).
File dukungan cakupan bergantung pada label dalam dependensi implisit sehingga dapat diganti oleh kebijakan pemanggilan, yang memungkinkan file tersebut berbeda di antara berbagai versi Bazel. Idealnya, perbedaan ini akan dihapus, dan kita akan menstandarkan salah satunya.
Kami juga membuat "laporan cakupan" yang menggabungkan cakupan yang dikumpulkan untuk setiap pengujian dalam pemanggilan Bazel. Hal ini ditangani oleh
CoverageReportActionFactory
dan dipanggil dari BuildView.createResult()
. Akses ke alat yang diperlukan diperoleh dengan melihat atribut :coverage_report_generator
dari 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
Setiap opsi ini diimplementasikan dengan membuat subclass AbstractBlazeQueryEnvironment
.
Fungsi kueri tambahan lainnya dapat dilakukan dengan membuat subclass QueryFunction
. Untuk mengizinkan hasil kueri streaming, bukan mengumpulkan hasil 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. Class ini diimplementasikan sebagai subclass dari
OutputFormatter
.
Persyaratan halus dari beberapa format output kueri (proto, pasti) adalah Bazel perlu mengeluarkan _semua _informasi yang disediakan oleh 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 umum adalah menggunakan label, dan melampirkan informasi kompleks ke aturan dengan label tersebut. Solusi ini tidak terlalu memuaskan dan akan sangat bagus jika persyaratan ini dihilangkan.
Sistem modul
Bazel dapat diperluas dengan menambahkan modul ke dalamnya. Setiap modul harus membuat subkelas
BlazeModule
(nama ini adalah peninggalan sejarah Bazel saat masih
disebut Blaze) dan mendapatkan informasi tentang berbagai peristiwa selama eksekusi
perintah.
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 penawaran titik ekstensi BlazeModule
agak tidak teratur. Jangan
menggunakannya sebagai contoh prinsip desain yang baik.
Bus peristiwa
Cara utama BlazeModules berkomunikasi dengan Bazel lainnya adalah melalui bus peristiwa
(EventBus
): instance baru dibuat untuk setiap build, berbagai bagian Bazel
dapat memposting peristiwa ke bus tersebut dan modul dapat mendaftarkan pemroses untuk peristiwa yang
diminati. Misalnya, hal-hal berikut direpresentasikan sebagai peristiwa:
- Daftar target build yang akan dibuat telah ditentukan
(
TargetParsingCompleteEvent
) - Konfigurasi tingkat atas telah ditentukan
(
BuildConfigurationEvent
) - Target dibuat, berhasil atau tidak (
TargetCompleteEvent
) - Pengujian telah dijalankan (
TestAttempt
,TestSummary
)
Beberapa peristiwa ini ditampilkan di luar Bazel dalam
Build Event Protocol
(yaitu BuildEvent
s). Hal ini memungkinkan tidak hanya BlazeModule
, tetapi juga hal-hal
di luar proses Bazel untuk mengamati build. File ini dapat diakses sebagai file yang berisi pesan protokol atau Bazel dapat terhubung ke server (yang disebut Build Event Service) untuk melakukan streaming peristiwa.
Hal ini diterapkan dalam paket Java build.lib.buildeventservice
dan
build.lib.buildeventstream
.
Repositori eksternal
Meskipun Bazel awalnya dirancang untuk digunakan dalam monorepo (pohon sumber tunggal yang berisi semua yang diperlukan untuk membangun), Bazel berada di dunia yang tidak selalu benar. "Repositori eksternal" adalah abstraksi yang digunakan untuk menghubungkan kedua dunia ini: repositori eksternal merepresentasikan kode yang diperlukan untuk build, tetapi tidak ada di pohon 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. Yang membuat hal ini menjadi rumit 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 (di
WorkspaceFileFunction
) dibagi menjadi beberapa bagian yang dibatasi oleh pernyataan load()
. Indeks chunk 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 menyebabkan Bazel membuat direktori di
$OUTPUT_BASE/external/<repository name>
.
Pengambilan 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 bahwa hal ini dilakukan untuk menghindari mendownload ulang sesuatu jika Skyframe dimulai ulang, tetapi ini bukan alasan yang kuat)RepositoryDelegatorFunction
mengetahui aturan repositori yang diminta untuk diambil dengan melakukan iterasi pada bagian-bagian file WORKSPACE hingga repositori yang diminta ditemukanRepositoryFunction
yang sesuai ditemukan yang mengimplementasikan pengambilan repositori;RepositoryFunction
tersebut adalah implementasi Starlark dari repositori atau peta hardcode untuk repositori yang diimplementasikan di Java.
Ada berbagai lapisan penyimpanan ke cache karena pengambilan repositori bisa sangat mahal:
- Ada cache untuk file yang didownload yang dikunci oleh checksum-nya
(
RepositoryCache
). Hal ini mengharuskan checksum tersedia di file WORKSPACE, tetapi hal ini bagus untuk hermetisitas. Direktori ini digunakan bersama oleh setiap instance server Bazel di workstation yang sama, terlepas dari ruang kerja atau dasar output yang dijalankannya. - "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, checksum tidak akan diambil ulang. Hal ini diterapkan diRepositoryDelegatorFunction.DigestWriter
. - Opsi command line
--distdir
menetapkan cache lain yang digunakan untuk mencari artefak yang akan didownload. Hal ini berguna dalam setelan perusahaan di mana Bazel tidak boleh mengambil hal-hal acak dari Internet. Hal ini diimplementasikan olehDownloadManager
.
Setelah repositori didownload, artefak di dalamnya 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
dibatalkan validasinya saat definisi repositori tempat artefak tersebut berada berubah. Dengan demikian,FileStateValue
s untuk artefak di repositori eksternal harus bergantung pada
repositori eksternalnya. Proses ini ditangani oleh ExternalFilesHelper
.
Pemetaan repositori
Beberapa repositori dapat bergantung pada repositori yang sama,
tetapi dalam versi yang berbeda (ini adalah contoh "masalah dependensi berlian"). Misalnya, jika dua biner di repositori terpisah dalam build
ingin bergantung pada Guava, keduanya mungkin akan merujuk ke Guava dengan label
yang dimulai dengan @guava//
dan berharap itu berarti versi yang berbeda.
Oleh karena itu, Bazel memungkinkan pemetaan ulang label repositori eksternal sehingga string @guava//
dapat merujuk ke satu repositori Guava (seperti @guava1//
) di repositori satu biner dan repositori Guava lain (seperti @guava2//
) di repositori biner lainnya.
Atau, fitur ini juga dapat digunakan untuk menggabungkan berlian. Jika repositori bergantung pada @guava1//
, dan repositori lain bergantung pada @guava2//
, pemetaan repositori memungkinkan salah satu repositori dipetakan ulang untuk menggunakan repositori @guava//
kanonis.
Pemetaan ditentukan dalam file WORKSPACE sebagai atribut repo_mapping
dari setiap definisi repositori. Kemudian, muncul di Skyframe sebagai anggota
WorkspaceFileValue
, yang terhubung ke:
Package.Builder.repositoryMapping
yang digunakan untuk mengubah atribut bernilai label dari aturan dalam paket denganRuleClass.populateRuleAttributeValues()
Package.repositoryMapping
yang digunakan dalam fase analisis (untuk menyelesaikan masalah seperti$(location)
yang tidak diuraikan dalam fase pemuatan)BzlLoadFunction
untuk menyelesaikan label dalam pernyataan load()
Bit JNI
Server Bazel sebagian besar ditulis dalam Java. Pengecualiannya adalah bagian yang tidak dapat dilakukan oleh Java sendiri atau tidak dapat dilakukan sendiri saat kami menerapkannya. Hal ini sebagian besar terbatas pada interaksi dengan sistem file, kontrol proses, dan berbagai hal tingkat rendah lainnya.
Kode C++ berada di bawah src/main/native dan class Java dengan metode native adalah:
NativePosixFiles
danNativePosixFileSystem
ProcessUtils
WindowsFileOperations
danWindowsFileProcesses
com.google.devtools.build.lib.platform
Output konsol
Memancarkan output konsol tampak seperti hal yang sederhana, tetapi pertemuan beberapa proses yang berjalan (terkadang dari jarak jauh), caching terperinci, keinginan untuk memiliki output terminal yang bagus dan berwarna, serta memiliki server yang berjalan lama membuatnya tidak sepele.
Segera 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 aliran ini. Kemudian, aliran ini diserahkan ke
BlazeCommandDispatcher.execExclusively()
.
Output dicetak secara default dengan urutan escape ANSI. Jika tidak diinginkan (--color=no
), elemen tersebut akan dihapus oleh AnsiStrippingOutputStream
. Selain itu, System.out
dan System.err
dialihkan ke aliran output ini.
Hal ini agar informasi proses debug dapat dicetak menggunakan
System.err.println()
dan tetap berada di output terminal klien
(yang berbeda dengan server). Dipastikan bahwa jika suatu proses menghasilkan output biner (seperti bazel query --output=proto
), tidak ada pemrosesan stdout.
Pesan singkat (error, peringatan, dan sejenisnya) ditampilkan melalui antarmuka EventHandler
. Yang perlu diperhatikan, hal ini berbeda dengan apa yang diposting ke
EventBus
(hal 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 penerapan EventHandler
menyimpan peristiwa yang diterima. Hal 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 yang di-cache.
Beberapa EventHandler
juga memungkinkan memposting acara yang pada akhirnya akan masuk ke bus acara (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 yang di-cache oleh ExtendedEventHandler
(sebaiknya dan
sebagian besar hal melakukannya; tetapi tidak diterapkan)
Output terminal sebagian besar dipancarkan melalui UiEventHandler
, yang
bertanggung jawab atas semua pemformatan output dan pelaporan progres yang dilakukan Bazel. Fungsi ini memiliki dua input:
- Bus peristiwa
- Aliran peristiwa yang disalurkan ke dalamnya melalui Reporter
Satu-satunya koneksi langsung yang dimiliki mekanisme eksekusi perintah (misalnya, Bazel lainnya) ke aliran RPC ke klien adalah melalui Reporter.getOutErr()
, yang memungkinkan akses langsung ke aliran ini. Opsi ini hanya digunakan saat perintah perlu membuang sejumlah besar kemungkinan data biner (seperti bazel query
).
Membuat Profil Bazel
Bazel berjalan dengan cepat. Bazel juga lambat, karena build cenderung berkembang hingga batas
yang masih dapat ditoleransi. Oleh karena itu, Bazel menyertakan profiler yang dapat
digunakan untuk memprofilkan build dan Bazel itu sendiri. Metode ini diterapkan dalam class yang
bernama Profiler
. Fitur ini diaktifkan secara default, meskipun hanya merekam data yang disingkat sehingga overhead-nya dapat ditoleransi; Command line
--record_full_profiler_data
membuatnya merekam semua yang dapat direkamnya.
File ini memancarkan profil dalam format profiler Chrome; sebaiknya dilihat di Chrome. Model datanya adalah model stack tugas: seseorang dapat memulai tugas dan mengakhiri tugas dan tugas tersebut harus disusun dengan rapi di dalam satu sama lain. Setiap thread Java mendapatkan stack tugasnya sendiri. TODO: Bagaimana cara kerjanya dengan tindakan dan gaya penerusan kelanjutan?
Profiler dimulai dan dihentikan di BlazeRuntime.initProfiler()
dan
BlazeRuntime.afterCommand()
masing-masing dan mencoba untuk aktif selama mungkin
agar kita dapat memprofilkan semuanya. Untuk menambahkan sesuatu ke profil,
panggil Profiler.instance().profile()
. Tindakan ini menampilkan Closeable
, yang penutupannya
mewakili akhir tugas. Sebaiknya digunakan dengan pernyataan try-with-resources.
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. Kita menyebut yang pertama "pengujian integrasi" dan yang kedua "pengujian unit", meskipun lebih seperti pengujian integrasi yang kurang terintegrasi. Kami juga memiliki beberapa pengujian unit sebenarnya, jika diperlukan.
Untuk pengujian integrasi, ada dua jenis:
- Yang diimplementasikan menggunakan framework pengujian bash yang sangat rumit di bawah
src/test/shell
- Yang diterapkan di Java. Class ini diimplementasikan sebagai subclass dari
BuildIntegrationTestCase
BuildIntegrationTestCase
adalah framework pengujian integrasi pilihan karena
dilengkapi dengan baik untuk sebagian besar skenario pengujian. Karena merupakan framework Java, Jetpack
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 sementara yang dapat Anda gunakan untuk menulis file BUILD
, lalu berbagai metode helper dapat meminta target yang dikonfigurasi, mengubah konfigurasi, dan menegaskan berbagai hal tentang hasil analisis.