Dokumen ini merupakan deskripsi codebase dan struktur Bazel. Fitur ini ditujukan bagi orang yang bersedia berkontribusi ke Bazel, bukan untuk pengguna akhir.
Pengantar
Codebase Bazel berukuran besar (~350 KLOC kode produksi dan ~260 kode pengujian KLOC) dan tidak seorang pun yang familier dengan seluruh lanskap: semua orang sangat memahami lembah khusus mereka, tetapi hanya sedikit yang tahu apa yang ada di atas bukit di setiap arah.
Agar orang-orang yang berada di tengah perjalanan tidak berada dalam kegelapan hutan dengan jalur yang jelas akan hilang, dokumen ini mencoba memberikan ringkasan codebase sehingga lebih mudah untuk memulai mengerjakannya.
Versi publik kode sumber Bazel tersedia di GitHub di github.com/bazelbuild/bazel. Ini bukan "sumber kebenaran"; ini berasal dari hierarki sumber internal Google yang berisi fungsi tambahan yang tidak berguna di luar Google. Tujuan jangka panjangnya adalah untuk menjadikan GitHub sebagai sumber kebenaran.
Kontribusi diterima melalui mekanisme permintaan pull GitHub reguler, dan diimpor secara manual oleh Googler ke hierarki sumber internal, lalu diekspor kembali ke GitHub.
Arsitektur klien/server
Sebagian besar Bazel berada dalam proses server yang tetap berada di RAM antar-build. Hal ini memungkinkan Bazel mempertahankan status di antara build.
Inilah sebabnya command line Bazel memiliki dua jenis opsi: startup dan perintah. Pada command line seperti ini:
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
Beberapa opsi (--host_jvm_args=
) muncul sebelum nama perintah yang akan dijalankan
dan beberapa opsi setelahnya (-c opt
); yang pertama disebut "opsi startup" dan
memengaruhi proses server secara keseluruhan, sedangkan jenis yang terakhir, "opsi
perintah", hanya memengaruhi satu perintah.
Setiap instance server memiliki satu hierarki sumber yang terkait ("ruang kerja") dan setiap ruang kerja biasanya memiliki satu instance server aktif. Hal ini dapat dihindari dengan menentukan basis output kustom (lihat bagian "Tata letak direktori" untuk mengetahui informasi selengkapnya).
Bazel didistribusikan sebagai 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. Layanan ini menyiapkan proses server yang sesuai menggunakan
langkah-langkah berikut:
- Memeriksa apakah sudah diekstrak. Jika tidak, sistem akan melakukannya. Dari sinilah asal server.
- Memeriksa apakah ada instance server aktif yang berfungsi: sedang berjalan,
memiliki opsi startup yang tepat, dan menggunakan direktori ruang kerja yang tepat. Server
yang ditemukan akan berjalan dengan melihat direktori
$OUTPUT_BASE/server
yang memiliki file kunci dengan port yang sedang diproses server. - Jika diperlukan, menghentikan proses server yang lama
- Jika perlu, mulai proses server baru
Setelah proses server yang sesuai siap, perintah yang perlu dijalankan
dikomunikasikan kepadanya melalui antarmuka gRPC, lalu output Bazel ditransfer kembali
ke terminal. Hanya satu perintah yang dapat berjalan secara bersamaan. Hal ini
diterapkan menggunakan mekanisme penguncian rumit dengan bagian di C++ dan 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 dalam BlazeRuntime
.
Di akhir perintah, server Bazel mengirimkan kode keluar yang harus ditampilkan
klien. Kerut yang menarik adalah implementasi bazel run
: tugas
perintah ini adalah menjalankan sesuatu yang baru saja dibuat oleh Bazel, tetapi tidak dapat melakukannya
dari proses server karena tidak memiliki terminal. Jadi, klien akan memberi tahu klien biner apa yang harus ujexec()
dan dengan argumen apa.
Saat menekan Ctrl-C, klien akan menerjemahkannya ke panggilan Cancel pada koneksi gRPC, yang akan 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 proses build. Deskripsi lengkap tersedia di Tata letak direktori output.
"workspace" adalah hierarki sumber Bazel yang dijalankan. Hal ini biasanya sesuai dengan sesuatu yang Anda periksa dari kontrol sumber.
Bazel menempatkan semua datanya di "root pengguna output". Hal ini biasanya
$HOME/.cache/bazel/_bazel_${USER}
, tetapi dapat diganti menggunakan
opsi startup --output_user_root
.
"Basis penginstalan" adalah tempat Bazel diekstrak. Hal ini dilakukan secara otomatis
dan setiap versi Bazel mendapatkan subdirektori berdasarkan checksumnya di
dasar penginstalan. Tombol ini berada di $OUTPUT_USER_ROOT/install
secara default dan dapat diubah
menggunakan opsi command line --install_base
.
"Output base" adalah tempat instance Bazel yang terpasang ke
ruang kerja tertentu menulis. Setiap basis output memiliki maksimal satu instance server Bazel
yang berjalan kapan saja. Harga tiketnya biasanya $OUTPUT_USER_ROOT/<checksum of the path
to the workspace>
. Ini dapat diubah menggunakan opsi startup --output_base
,
yaitu, antara lain, berguna untuk mengatasi batasan bahwa hanya
satu instance Bazel yang dapat berjalan di ruang kerja kapan saja.
Direktori output berisi, antara lain:
- Repositori eksternal yang diambil di
$OUTPUT_BASE/external
. - Root exec, direktori yang berisi symlink ke semua kode
sumber untuk build saat ini. Lokasinya di
$OUTPUT_BASE/execroot
. Selama build, direktori kerja adalah$EXECROOT/<name of main repository>
. Kami berencana untuk mengubahnya menjadi$EXECROOT
, meskipun ini merupakan rencana jangka panjang karena merupakan perubahan yang sangat tidak kompatibel. - File yang di-build selama proses build.
Proses mengeksekusi perintah
Setelah server Bazel mendapatkan kontrol dan diberi tahu tentang perintah yang perlu dieksekusi, urutan peristiwa berikut akan terjadi:
BlazeCommandDispatcher
akan diberi tahu tentang permintaan baru tersebut. Aturan ini menentukan apakah perintah memerlukan ruang kerja untuk dijalankan (hampir setiap perintah kecuali yang tidak berhubungan dengan kode sumber, seperti versi atau bantuan), dan apakah perintah lain berjalan atau tidak.Perintah yang tepat ditemukan. Setiap perintah harus mengimplementasikan antarmuka
BlazeCommand
dan harus memiliki anotasi@Command
(ini adalah sedikit antipola, 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 acara dibuat. Bus peristiwa adalah aliran data untuk peristiwa yang terjadi selama build. Beberapa di antaranya diekspor ke luar Bazel berdasarkan agama Protokol Peristiwa Build untuk memberi tahu dunia tentang performa build.
Perintah akan mendapatkan kontrol. Perintah yang paling menarik adalah perintah yang menjalankan build: build, pengujian, jalankan, cakupan, dan sebagainya: fungsi ini diterapkan oleh
BuildTool
.Kumpulan pola target di command line diuraikan dan karakter pengganti seperti
//pkg:all
dan//pkg/...
diselesaikan. Hal ini diterapkan diAnalysisPhaseRunner.evaluateTargetPatterns()
dan digabungkan di Skyframe sebagaiTargetPatternPhaseValue
.Fase pemuatan/analisis dijalankan untuk menghasilkan grafik tindakan (grafik perintah asiklik terarah yang perlu dijalankan untuk build).
Fase eksekusi sedang dijalankan. Ini berarti menjalankan setiap tindakan yang diperlukan untuk mem-build target level teratas yang diminta akan dijalankan.
Opsi command line
Opsi command line untuk pemanggilan Bazel dijelaskan dalam
objek OptionsParsingResult
, yang kemudian berisi peta dari "class
opsi" ke nilai opsi. "Class opsi" adalah subclass OptionsBase
dan mengelompokkan opsi command line yang terkait satu sama lain. Contoh:
- Opsi yang terkait dengan bahasa pemrograman (
CppOptions
atauJavaOptions
). Ini harus menjadi subclass dariFragmentOptions
dan akhirnya digabungkan ke dalam objekBuildOptions
. - Opsi yang terkait dengan cara Bazel mengeksekusi tindakan (
ExecutionOptions
)
Opsi ini dirancang untuk digunakan dalam fase analisis dan (melalui RuleContext.getFragment()
di Java atau ctx.fragments
di Starlark).
Beberapa di antaranya (misalnya, apakah C++ menyertakan pemindaian atau tidak) dibaca
dalam fase eksekusi, tetapi hal itu selalu memerlukan pipeline eksplisit karena
BuildConfiguration
tidak akan tersedia. Untuk mengetahui informasi selengkapnya, lihat
bagian "Konfigurasi".
PERINGATAN: Kami ingin berpura-pura bahwa instance OptionsBase
tidak dapat diubah dan
menggunakannya dengan cara itu (seperti bagian dari SkyKeys
). Tidak benar dan
mengubahnya adalah cara yang sangat baik untuk merusak Bazel dengan cara halus yang sulit
untuk di-debug. Sayangnya, upaya tersebut tidak dapat diubah.
(Mengubah FragmentOptions
segera setelah pembuatan sebelum orang lain
mendapatkan kesempatan untuk menyimpan referensi ke dalamnya dan sebelum equals()
atau hashCode()
disebut saja tidak masalah.)
Bazel mempelajari class opsi dengan cara berikut:
- Beberapa kabel disambungkan ke 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
subclass FragmentOptions
yang memiliki anotasi @Option
, yang menentukan
nama dan jenis opsi command line beserta beberapa teks bantuan.
Jenis Java nilai opsi command line biasanya adalah hal sederhana (string, bilangan bulat, Boolean, label, dll.). Namun, kami juga mendukung
opsi jenis yang lebih rumit; dalam hal ini, tugas melakukan konversi dari
string command line ke jenis data bergantung pada implementasi
com.google.devtools.common.options.Converter
.
Pohon sumber, seperti yang dilihat oleh Bazel
Bazel terlibat dalam bisnis pembuatan software, yang terjadi dengan membaca dan menafsirkan kode sumber. Totalitas kode sumber yang dioperasikan Bazel disebut "ruang kerja" dan terstruktur ke dalam repositori, paket, dan aturan.
Repositories
"Repositori" adalah hierarki sumber tempat developer bekerja; biasanya mewakili satu project. Ancestor Bazel, Blaze, beroperasi pada monorepo, yaitu, satu pohon sumber yang berisi semua kode sumber yang digunakan untuk menjalankan build. Sebaliknya, Bazel mendukung project yang kode sumbernya mencakup beberapa repositori. Repositori tempat Bazel dipanggil disebut "repositori utama", yang lainnya disebut "repositori eksternal".
Repositori ditandai dengan file yang disebut WORKSPACE
(atau WORKSPACE.bazel
) di
direktori utama. File ini berisi informasi yang bersifat "global" untuk seluruh
build, misalnya kumpulan repositori eksternal yang tersedia. Ini berfungsi seperti
file Starlark biasa yang berarti bahwa seseorang dapat load()
file Starlark lainnya.
Ini biasanya digunakan untuk mengambil repositori yang diperlukan oleh repositori
yang direferensikan secara eksplisit (kami menyebutnya "pola deps.bzl
")
Kode repositori eksternal terhubung atau didownload di
$OUTPUT_BASE/external
.
Saat menjalankan build, seluruh hierarki sumber perlu disatukan; ini
dilakukan oleh SymlinkForest
, yang menghubungkan setiap paket di repositori utama
ke $EXECROOT
dan setiap repositori eksternal ke $EXECROOT/external
atau
$EXECROOT/..
(yang sebelumnya tentu tidak dapat memiliki paket
yang disebut external
di repositori utama; itulah sebabnya kami bermigrasi dari
nya)
Paket
Setiap repositori terdiri dari paket, sekumpulan file terkait, dan
spesifikasi dependensi. Class ini ditentukan oleh file bernama
BUILD
atau BUILD.bazel
. Jika keduanya ada, Bazel lebih memilih BUILD.bazel
; alasan
BUILD
file masih diterima adalah ancestor Bazel, Blaze, menggunakan
nama file ini. Namun, ternyata segmen tersebut umum digunakan, terutama
di Windows, karena nama file tidak peka huruf besar/kecil.
Paket tidak saling bergantung: perubahan pada file BUILD
paket
tidak dapat menyebabkan perubahan paket lainnya. Penambahan atau penghapusan file BUILD
_dapat_mengubah paket lain, karena glob rekursif berhenti pada batas paket
sehingga adanya file BUILD
akan menghentikan rekursi.
Evaluasi file BUILD
disebut "pemuatan paket". Implementasi ini
di class PackageFactory
, berfungsi dengan memanggil penafsir Starlark dan
memerlukan pengetahuan tentang kumpulan class aturan yang tersedia. Hasil pemuatan
paket adalah objek Package
. Umumnya menggunakan peta dari string (nama target) ke target itu sendiri.
Sebagian besar kerumitan 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, paket ini mendukung glob berulang yang
turun ke subdirektori (tetapi tidak masuk ke dalam sub-paket). Hal ini memerlukan akses ke
sistem file dan karena operasi tersebut dapat berjalan lambat, kami menerapkan semua jenis trik untuk
membuatnya berjalan secara paralel dan seefisien mungkin.
Globbing diterapkan di class berikut:
LegacyGlobber
, globber yang tidak disadari Skyframe dengan cepat dan bahagiaSkyframeHybridGlobber
, versi yang menggunakan Skyframe dan kembali ke globber lama untuk menghindari "Mulai ulang Skyframe" (dijelaskan di bawah)
Class Package
itu sendiri berisi beberapa anggota yang secara eksklusif digunakan untuk
mengurai file WORKSPACE dan yang tidak cocok untuk paket sebenarnya. Ini adalah kelemahan desain karena objek yang menjelaskan paket reguler tidak boleh berisi kolom yang menjelaskan sesuatu yang lain. Di antaranya meliputi:
- Pemetaan repositori
- Toolchain terdaftar
- Platform eksekusi terdaftar
Idealnya, akan ada lebih banyak pemisahan antara penguraian file WORKSPACE agar
tidak menguraikan 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:
- Files: hal-hal yang merupakan input atau output build. Dalam bahasa Bazel, kami menyebutnya artefak (dibahas di tempat lain). Tidak semua file yang dibuat selama proses build merupakan target; biasanya output Bazel tidak memiliki label terkait.
- Aturan: ini menggambarkan langkah-langkah untuk mendapatkan outputnya dari inputnya. Umumnya dikaitkan dengan bahasa pemrograman (seperti
cc_library
,java_library
, ataupy_library
), tetapi ada beberapa bahasa tanpa bahasa (sepertigenrule
ataufilegroup
) - Grup paket: yang 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 pada 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 dalam paket direktori kerja saat ini (jalur relatif yang berisi referensi tingkat atas (..) tidak diizinkan)
Jenis aturan (seperti "library C++") disebut sebagai "class aturan". Class aturan dapat
diterapkan di Starlark (fungsi rule()
) atau di Java (disebut
"aturan native", ketik RuleClass
). Dalam jangka panjang, setiap aturan khusus
bahasa akan diterapkan di Starlark, tetapi untuk beberapa keluarga aturan lama (seperti Java
atau C++) masih ada di Java.
Class aturan Starlark harus diimpor di awal file
BUILD
menggunakan pernyataan load()
, sedangkan class aturan Java "secara bawaan" dikenal oleh
Bazel, berdasarkan pendaftarannya dengan ConfiguredRuleClassProvider
.
Class aturan berisi informasi seperti:
- Atribut-nya (seperti
srcs
,deps
): jenis, nilai default, batasan, dll. - Transisi dan aspek konfigurasi yang disertakan pada setiap atribut, jika ada
- Implementasi aturan
- Penyedia info transit yang "biasanya" membuat aturan
Catatan terminologi: Di codebase, kami sering menggunakan "Aturan" untuk berarti 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; targetnya
hanya berupa "target". Perhatikan juga bahwa meskipun RuleClass
memiliki "class" dalam
namanya, tidak ada hubungan pewarisan Java antara class aturan dan target
jenis tersebut.
Bingkai langit
Framework evaluasi yang mendasari Bazel disebut Skyframe. Modelnya adalah semua hal yang perlu di-build selama proses build diatur ke dalam grafik asiklik terarah dengan tepi yang mengarah dari setiap bagian data ke dependensinya, yaitu, bagian lain dari data yang perlu diketahui untuk membuatnya.
Node dalam grafik disebut SkyValue
dan namanya disebut SkyKey
. Keduanya tidak dapat diubah; hanya objek yang tidak dapat diubah yang
dapat dijangkau darinya. Invarian ini hampir selalu ada, dan jika tidak
(seperti untuk class opsi individual 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.
Oleh karena itu, semua hal yang dihitung dalam Skyframe (seperti
target yang dikonfigurasi) juga tidak dapat diubah.
Cara yang paling mudah untuk mengamati grafik Skyframe adalah dengan menjalankan bazel dump
--skyframe=deps
, yang membuang grafik, satu SkyValue
per baris. Sebaiknya
lakukan untuk build kecil, karena ukurannya bisa sangat besar.
Skyframe berada dalam paket com.google.devtools.build.skyframe
. Paket
yang bernama mirip com.google.devtools.build.lib.skyframe
berisi
implementasi Bazel di atas Skyframe. Informasi lebih lanjut tentang Skyframe
tersedia di sini.
Untuk mengevaluasi SkyKey
tertentu menjadi SkyValue
, Skyframe akan memanggil SkyFunction
yang sesuai dengan jenis kunci. Selama evaluasi fungsi, fungsi mungkin meminta dependensi lain dari Skyframe dengan memanggil berbagai overload SkyFunction.Environment.getValue()
. Hal ini memiliki efek samping dari pendaftaran dependensi tersebut ke grafik internal Skyframe, sehingga Skyframe akan mengetahui untuk mengevaluasi ulang fungsi tersebut saat salah satu dependensinya berubah. Dengan kata lain, caching dan komputasi inkremental Skyframe berfungsi pada
detail SkyFunction
dan SkyValue
.
Setiap kali SkyFunction
meminta dependensi yang tidak tersedia, getValue()
akan menampilkan null. Selanjutnya, fungsi tersebut harus menghasilkan kontrol kembali ke Skyframe dengan menampilkan null. Pada tahap berikutnya, 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, semua komputasi yang dilakukan di dalam SkyFunction
sebelum mulai ulang harus diulang. Namun, tindakan ini tidak mencakup pekerjaan yang dilakukan untuk
mengevaluasi dependensi SkyValues
, yang di-cache. Oleh karena itu, biasanya kami mengatasi
masalah ini dengan:
- Mendeklarasikan dependensi dalam batch (dengan menggunakan
getValuesAndExceptions()
) untuk membatasi jumlah mulai ulang. - Memecah
SkyValue
menjadi beberapa 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 antar-mulai ulang, baik menggunakan
SkyFunction.Environment.getState()
maupun mempertahankan cache statis ad hoc "di belakang Skyframe".
Pada dasarnya, kita membutuhkan jenis solusi ini karena kita secara rutin memiliki ratusan ribu node Skyframe yang sedang beroperasi, dan Java tidak mendukung thread ringan.
Starlark
Starlark adalah bahasa khusus domain yang digunakan orang untuk mengonfigurasi dan memperluas Bazel. Ini dianggap sebagai subset Python yang dibatasi yang memiliki jenis yang jauh lebih sedikit, lebih banyak batasan pada alur kontrol, dan yang paling penting, jaminan yang tidak dapat diubah untuk mengaktifkan pembacaan serentak. Hal ini bukanlah proses Turing, yang tidak menyarankan beberapa (tetapi tidak semua) pengguna mencoba menyelesaikan tugas pemrograman umum dalam bahasa tersebut.
Starlark diterapkan dalam paket net.starlark.java
.
Layanan ini juga memiliki implementasi Go independen
di sini. Implementasi Java
yang digunakan dalam Bazel saat ini menjadi 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 itu sendiri dan file.bzl
yang dimuat olehnya. - Definisi aturan. Begitulah 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 akan dibahas nanti).
- File WORKSPACE. Di sinilah repositori eksternal (kode yang tidak berada di 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 keduanya mengekspresikan hal yang berbeda. Daftar perbedaan tersedia
di sini.
Informasi lebih lanjut tentang Starlark tersedia di sini.
Fase pemuatan/analisis
Fase pemuatan/analisis adalah tempat Bazel menentukan tindakan yang diperlukan untuk membuat aturan tertentu. Unit dasarnya adalah "target yang dikonfigurasi", yang, cukup masuk akal, pasangan (target, konfigurasi).
Hal ini disebut "fase pemuatan/analisis" karena dapat dibagi menjadi dua bagian yang berbeda, yang sebelumnya diserialisasi, tetapi sekarang dapat tumpang tindih:
- Memuat paket, yaitu mengubah file
BUILD
menjadi objekPackage
yang mewakilinya - Menganalisis target yang dikonfigurasi, yaitu menjalankan penerapan aturan untuk menghasilkan grafik tindakan
Setiap target yang dikonfigurasi di penutupan transit target yang dikonfigurasi yang diminta pada command line harus dianalisis dari bawah ke atas; yaitu, node daun terlebih dahulu, lalu ke node di command line. Input untuk analisis satu target yang dikonfigurasi adalah:
- Konfigurasi. ("bagaimana" membangun aturan itu; misalnya, platform target tetapi juga beberapa hal seperti opsi command line yang ingin diteruskan ke compiler C++)
- Dependensi langsung. Penyedia info transitifnya tersedia untuk aturan yang sedang dianalisis. Disebut seperti itu karena menyediakan "roll-up" informasi di penutupan transitif target yang dikonfigurasi, seperti semua file .jar di classpath atau semua file .o yang perlu ditautkan ke biner C++)
- Targetnya. Ini adalah hasil dari pemuatan paket tempat target berada. Untuk aturan, aturan ini mencakup atributnya, yang biasanya penting.
- Penerapan target yang dikonfigurasi. Untuk aturan, lokasi dapat berada di Starlark atau di Java. Semua target yang tidak dikonfigurasi sesuai aturan akan diterapkan di Java.
Output analisis target yang dikonfigurasi adalah:
- Penyedia info transitif yang mengonfigurasi target yang bergantung padanya dapat mengakses
- Artefak yang dapat dibuat dan tindakan yang menghasilkannya.
API yang ditawarkan pada aturan Java adalah RuleContext
, yang setara dengan argumen ctx
aturan Starlark. API ini lebih canggih, tetapi pada saat yang sama, lebih mudah untuk melakukan Bad ThingsTM, misalnya, untuk menulis kode yang waktu atau kompleksitas ruangnya kuadrat (atau lebih buruk), membuat server Bazel error dengan pengecualian Java atau melanggar invarian (seperti dengan memodifikasi instance Options
secara tidak sengaja atau membuat target yang dikonfigurasi dapat berubah)
Algoritme yang menentukan dependensi langsung dari target yang dikonfigurasi
berada di DependencyResolver.dependentNodeMap()
.
Konfigurasi
Konfigurasi adalah "bagaimana" mem-build target: untuk platform apa, dengan opsi command line apa pun, 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 kami melakukan kompilasi silang atau saat kami membuat aplikasi Android lemak (yang berisi kode native untuk beberapa arsitektur CPU)
Secara konseptual, konfigurasinya adalah instance BuildOptions
. Namun, dalam praktiknya, BuildOptions
digabungkan oleh BuildConfiguration
yang menyediakan berbagai fungsi tambahan. Kode ini menyebar dari bagian atas
grafik dependensi ke bagian bawah. Jika berubah, build perlu
dianalisis ulang.
Hal ini mengakibatkan anomali seperti harus menganalisis ulang seluruh build jika, misalnya, jumlah pengujian yang diminta berubah, meskipun itu hanya memengaruhi target pengujian (kami memiliki rencana untuk "memangkas" konfigurasi sehingga hal ini tidak terjadi, tetapi belum siap).
Jika implementasi aturan memerlukan bagian konfigurasi, aturan tersebut harus mendeklarasikan
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". Proses perubahan 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 mendeklarasikan bahwa dependensi tertentu digunakan selama build, dan sehingga harus dibuat dalam arsitektur eksekusi
- Untuk mendeklarasikan bahwa dependensi tertentu harus di-build untuk beberapa arsitektur (seperti untuk kode native di APK Android gemuk)
Jika transisi konfigurasi menghasilkan beberapa konfigurasi, hal ini disebut transisi terpisah.
Transisi konfigurasi juga dapat diterapkan di Starlark (dokumentasi di sini)
Penyedia info transitif
Penyedia info transitif adalah cara (dan _hanya _cara) target yang dikonfigurasi untuk memberi tahu hal-hal tentang target dikonfigurasi lainnya yang bergantung padanya. Alasan "transitif" ada di namanya adalah karena ini biasanya merupakan roll-up penutupan transitif dari target yang dikonfigurasi.
Umumnya ada korespondensi 1:1 antara penyedia info transitif Java
dan penyedia Starlark (pengecualiannya adalah DefaultInfo
yang merupakan penggabungan
FileProvider
, FilesToRunProvider
, dan RunfilesProvider
karena API tersebut
dianggap lebih cocok untuk Starlark daripada transliterasi langsung untuk Java).
Kuncinya adalah salah satu hal berikut:
- Objek Class Java. Opsi ini hanya tersedia untuk penyedia yang tidak
dapat diakses dari Starlark. Penyedia ini adalah subclass
TransitiveInfoProvider
. - String. Ini adalah versi lama dan sangat tidak dianjurkan karena rentan terhadap konflik nama. Penyedia info transitif tersebut merupakan subclass langsung dari
build.lib.packages.Info
. - Simbol penyedia. Penyedia ini dapat dibuat dari Starlark menggunakan fungsi
provider()
dan merupakan cara yang direkomendasikan untuk membuat penyedia baru. Simbol ini diwakili oleh instanceProvider.Key
di Java.
Penyedia baru yang diimplementasikan di Java harus diterapkan 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. Ini terdiri dari hal-hal berikut:
filesToBuild
mereka, konsep samar dari "kumpulan file yang direpresentasikan oleh aturan ini". Ini adalah file yang di-build saat target yang dikonfigurasi ada di command line atau dalam src genrule.- Runfile, reguler, dan data.
- Grup output-nya. Berikut adalah berbagai "kumpulan file lain" yang dapat dibuat oleh aturan. File tersebut dapat diakses menggunakan atribut output_group pada
aturan filegroup di BUILD dan menggunakan penyedia
OutputGroupInfo
di Java.
File run
Beberapa biner memerlukan file data agar dapat dijalankan. Contoh yang jelas adalah pengujian yang memerlukan file input. Ini diwakili oleh Bazel dengan konsep "runfiles". "Runfiles tree" adalah hierarki direktori file data untuk biner tertentu. File ini dibuat dalam sistem file sebagai hierarki symlink dengan symlink individual yang menunjuk ke file dalam sumber hierarki output.
Kumpulan runfile direpresentasikan sebagai instance Runfiles
. Secara konsep, 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 runfiles file sama dengan execpath-nya. Kami menggunakannya untuk menghemat RAM.
- Ada berbagai jenis entri lama dalam hierarki runfile, yang juga perlu diwakili.
Runfile dikumpulkan menggunakan RunfilesProvider
: instance class
ini merepresentasikan runfile, target yang dikonfigurasi (seperti library) dan kebutuhan
penutupan transitifnya, serta dikumpulkan seperti set bertingkat (sebenarnya,
implementasi tersebut diterapkan menggunakan set bertingkat di bawah sampul): setiap target menyatukan
file runing dependensinya, menambahkan beberapa dependensinya sendiri, 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 menyajikan runfile yang berbeda saat bergantung melalui atribut data dibandingkan dengan yang sebaliknya. Ini adalah perilaku lama yang tidak diinginkan yang 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). Hal ini
memerlukan komponen tambahan berikut:
- Manifes file run input. Ini adalah deskripsi serial hierarki runfile. File ini digunakan sebagai proxy untuk konten hierarki runfile dan Bazel mengasumsikan bahwa hierarki runfile berubah jika dan hanya jika konten manifes berubah.
- Manifes runfiles output. API ini digunakan oleh library runtime yang menangani hierarki runfile, terutama di Windows, yang terkadang tidak mendukung link simbolis.
- Penjual runfile. Agar pohon runfiles ada, seseorang harus membuat hierarki symlink dan artefak yang ditunjuk symlink. Untuk mengurangi jumlah tepi dependensi, perantara runfile dapat digunakan untuk merepresentasikan semua fungsi ini.
- Argumen command line untuk menjalankan biner yang runfile-nya mewakili
objek
RunfilesSupport
.
Aspek
Aspek adalah cara untuk "menerapkan komputasi ke bawah grafik dependensi". Ekstensi ini
dijelaskan untuk pengguna Bazel
di sini. Contoh
yang menarik adalah buffering protokol: aturan proto_library
tidak boleh mengetahui
bahasa tertentu, tetapi mem-build implementasi pesan
buffering protokol ("unit dasar" 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, aturan tersebut hanya akan dibuat sekali.
Sama seperti target yang dikonfigurasi, target tersebut diwakili di Skyframe sebagai SkyValue
dan cara pembuatannya sangat mirip dengan cara mem-build target yang dikonfigurasi: mereka memiliki class factory bernama ConfiguredAspectFactory
yang memiliki akses ke RuleContext
, tetapi tidak seperti factory target yang dikonfigurasi, class ini juga mengetahui target yang dikonfigurasi yang dikaitkan dengan dan penyedianya.
Kumpulan aspek yang disebarkan ke grafik dependensi ditentukan untuk setiap
atribut menggunakan fungsi Attribute.Builder.aspects()
. Ada beberapa
class bernama membingungkan yang berpartisipasi dalam proses ini:
AspectClass
adalah penerapan aspek. Class ini dapat berada di Java (dalam hal ini subclass) atau di Starlark (dalam hal ini instance dariStarlarkAspectClass
). Serupa denganRuleConfiguredTargetFactory
.AspectDefinition
adalah definisi aspek; itu mencakup penyedia yang diperlukan, penyedia yang disediakannya dan berisi referensi ke implementasinya, seperti instanceAspectClass
yang sesuai. Ini serupa denganRuleClass
.AspectParameters
adalah cara untuk menganalisis aspek yang disebarkan ke grafik dependensi. Saat ini berupa string ke peta string. Contoh yang baik tentang manfaatnya adalah buffering protokol: jika suatu bahasa memiliki beberapa API, informasi tentang API yang harus dibuat buffering protokol harus disebarkan ke bawah grafik dependensi.Aspect
mewakili semua data yang diperlukan untuk menghitung aspek yang menyebarkan grafik dependensi. Ini terdiri dari class aspek, definisinya, dan parameternya.RuleAspect
adalah fungsi yang menentukan aspek mana yang harus diterapkan oleh aturan tertentu. Ini adalah fungsiRule
->Aspect
.
Detail yang agak tidak terduga adalah bahwa aspek dapat dilampirkan ke aspek lain;
misalnya, aspek yang mengumpulkan classpath untuk Java IDE mungkin
ingin mengetahui tentang semua file .jar di classpath, tetapi beberapa di antaranya adalah
buffering protokol. Dalam hal ini, aspek IDE ingin dilampirkan ke
pasangan (aturan proto_library
+ aspek proto Java).
Kompleksitas aspek-aspek tersebut tercakup dalam class AspectCollection
.
Platform dan toolchain
Bazel mendukung build multi-platform, yaitu build yang mungkin memiliki beberapa arsitektur tempat tindakan build dijalankan dan beberapa arsitektur yang di-build kodenya. Arsitektur ini disebut sebagai platform dalam bahasa Bazel (dokumentasi lengkap di sini)
Platform dijelaskan melalui pemetaan nilai kunci dari setelan batasan (seperti
konsep "arsitektur CPU") hingga nilai batasan (seperti CPU
tertentu seperti x86_64). Kami memiliki "kamus" dari setelan dan nilai
batasan yang paling umum digunakan di repositori @platforms
.
Konsep Toolchain berasal dari fakta bahwa bergantung pada platform yang digunakan untuk menjalankan build dan platform yang ditargetkan, seseorang mungkin perlu menggunakan compiler yang berbeda; misalnya, toolchain C++ tertentu dapat berjalan pada OS tertentu dan dapat menargetkan beberapa OS lainnya. Bazel harus menentukan compiler C++ yang digunakan berdasarkan eksekusi set dan platform target (dokumentasi untuk toolchain di sini).
Untuk melakukannya, toolchain dianotasi dengan kumpulan eksekusi dan batasan platform target yang didukungnya. Untuk melakukannya, definisi toolchain dibagi menjadi dua bagian:
- Aturan
toolchain()
yang menjelaskan kumpulan eksekusi dan batasan target yang didukung toolchain dan memberi tahu jenis apa (seperti C++ atau Java) dari toolchain tersebut (yang terakhir diwakili oleh aturantoolchain_type()
) - Aturan spesifik per bahasa yang menjelaskan toolchain sebenarnya (seperti
cc_toolchain()
)
Hal ini dilakukan dengan cara ini karena kita perlu mengetahui batasan untuk setiap toolchain agar dapat melakukan resolusi toolchain dan aturan *_toolchain()
khusus bahasa berisi lebih banyak informasi daripada itu, sehingga memerlukan waktu lebih lama untuk dimuat.
Platform eksekusi ditentukan dengan salah satu cara berikut:
- Di 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
kita pada akhirnya ingin mendukung beberapa platform target, tetapi belum diimplementasikan.
Kumpulan toolchain yang akan digunakan untuk target yang dikonfigurasi ditentukan oleh
ToolchainResolutionFunction
. Ini adalah fungsi dari:
- Kumpulan toolchain terdaftar (di file WORKSPACE dan konfigurasi)
- Platform eksekusi dan target yang diinginkan (di 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 adalah peta dari jenis toolchain (direpresentasikan sebagai instance ToolchainTypeInfo
) ke label toolchain yang dipilih. Disebut "unload" karena tidak berisi
funnel itu sendiri, hanya labelnya.
Kemudian, toolchain sebenarnya dimuat menggunakan ResolvedToolchainContext.load()
dan digunakan oleh penerapan target yang dikonfigurasi yang memintanya.
Kami juga memiliki sistem lama yang bergantung pada keberadaan satu konfigurasi "host"
dan konfigurasi target yang direpresentasikan oleh berbagai
flag konfigurasi, seperti --cpu
. Kami bertransisi secara bertahap ke sistem
di atas. Untuk menangani kasus saat orang mengandalkan nilai konfigurasi
lama, kami telah menerapkan
pemetaan platform
untuk menerjemahkan antara flag lama dan batasan platform gaya baru.
Kode mereka menggunakan PlatformMappingFunction
dan menggunakan "bahasa
kecil" non-Starlark.
Batasan
Terkadang, satu orang ingin menetapkan target sebagai kompatibel dengan beberapa platform saja. Bazel memiliki (sayangnya) beberapa mekanisme untuk mencapai tujuan ini:
- Batasan khusus aturan
environment_group()
/environment()
- Batasan platform
Batasan khusus aturan sebagian besar digunakan dalam aturan Google untuk Java. Batasan tersebut sedang dalam proses keluar dan tidak tersedia di Bazel, tetapi kode sumbernya mungkin berisi referensi ke aturan tersebut. Atribut yang mengatur hal ini disebut
constraints=
.
lingkungan_grup() dan lingkungan()
Aturan ini adalah mekanisme lama dan tidak digunakan secara luas.
Semua aturan build dapat mendeklarasikan "lingkungan" tempat elemen tersebut dapat dibuat, dan "lingkungan" adalah instance dari aturan environment()
.
Ada berbagai cara lingkungan yang didukung dapat ditentukan untuk aturan:
- Melalui atribut
restricted_to=
. Ini adalah bentuk spesifikasi paling langsung; mendeklarasikan kumpulan lingkungan persis yang didukung aturan untuk grup ini. - Melalui atribut
compatible_with=
. Tindakan 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 pembanding terkait tematik (seperti "arsitektur CPU", "versi JDK", atau "sistem operasi seluler"). Definisi grup lingkungan mencakup lingkungan mana yang harus didukung oleh "default" jika tidak ditentukan oleh atributrestricted_to=
/environment()
. Aturan tanpa atribut tersebut akan mewarisi semua setelan default. - Melalui default class aturan. Tindakan ini mengganti default global untuk semua instance class aturan tertentu. Ini dapat digunakan, misalnya, untuk membuat
semua aturan
*_test
dapat diuji tanpa setiap instance harus mendeklarasikan kemampuan ini secara eksplisit.
environment()
diimplementasikan sebagai aturan reguler, sedangkan environment_group()
adalah subclass Target
, tetapi bukan Rule
(EnvironmentGroup
) dan
fungsi yang tersedia secara default dari Starlark
(StarlarkLibrary.environmentGroup()
) yang akhirnya membuat target
eponim. Hal ini untuk menghindari dependensi siklik yang akan muncul, karena setiap lingkungan harus mendeklarasikan grup lingkungannya dan setiap grup lingkungan harus mendeklarasikan lingkungan defaultnya.
Build dapat dibatasi pada lingkungan tertentu dengan
opsi command line --target_environment
.
Implementasi pemeriksaan batasan ada di
RuleContextConstraintSemantics
dan TopLevelConstraintSemantics
.
Batasan platform
Cara "resmi" saat ini untuk mendeskripsikan platform yang kompatibel dengan target adalah dengan menggunakan batasan yang sama dengan yang digunakan untuk mendeskripsikan toolchain dan platform. Sedang dalam peninjauan pada permintaan pull #10945.
Visibilitas
Jika Anda mengerjakan codebase besar dengan banyak developer (seperti di Google), Anda harus berhati-hati untuk mencegah orang lain secara bebas bergantung pada kode Anda. Jika tidak, sesuai dengan hukum Hiruma, orang akan bergantung pada perilaku yang Anda anggap sebagai detail implementasi.
Bazel mendukung hal ini dengan mekanisme yang disebut visibilitas: Anda dapat mendeklarasikan bahwa target tertentu hanya dapat bergantung pada penggunaan atribut visibilitas. Atribut ini sedikit khusus karena meskipun memiliki daftar label, label ini dapat mengenkode pola di atas nama paket, bukan pointer ke target tertentu. (Ya, ini adalah kelemahan desain.)
Hal ini diterapkan di tempat berikut:
- Antarmuka
RuleVisibility
mewakili deklarasi visibilitas. Hal ini bisa 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 subhierarki paket (//pkg:__subpackages__
). Ini berbeda dengan sintaksis command line, yang menggunakan//pkg:*
atau//pkg/...
. - Grup paket diimplementasikan sebagai targetnya sendiri (
PackageGroup
) dan target yang dikonfigurasi (PackageGroupConfiguredTarget
). Kita mungkin dapat menggantinya dengan aturan sederhana jika mau. Logikanya diimplementasikan dengan bantuan:PackageSpecification
, yang sesuai dengan pola tunggal seperti//pkg/...
;PackageGroupContents
, yang sesuai dengan satu atributpackages
package_group
; danPackageSpecificationProvider
, yang digabungkan melaluipackage_group
danincludes
transitifnya. - Konversi dari daftar label visibilitas ke dependensi dilakukan di
DependencyResolver.visitTargetVisibility
dan beberapa tempat lainnya. - Pemeriksaan sebenarnya dilakukan di
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
Set bertingkat
Sering kali, target yang dikonfigurasi menggabungkan kumpulan file dari dependensinya, menambahkannya sendiri, dan menggabungkan kumpulan agregat tersebut 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 perlu ada di classpath, agar aturan Java dapat dikompilasi atau dijalankan
- Kumpulan file Python dalam penutupan transitif aturan Python
Jika kita melakukannya dengan cara yang 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 memunculkan konsep
NestedSet
. Ini adalah struktur data yang terdiri dari instance NestedSet
lainnya dan beberapa anggotanya sendiri, sehingga membentuk grafik asiklik terarah dari set. Keduanya tidak dapat diubah dan anggotanya dapat diiterasi. Kita menentukan
beberapa urutan iterasi (NestedSet.Order
): praorder, postorder, topologi
(node selalu muncul setelah ancestornya) dan "tidak peduli, tetapi harus
sama setiap saat".
Struktur data yang sama disebut depset
di Starlark.
Artefak dan Tindakan
Build sebenarnya terdiri dari serangkaian perintah yang perlu dijalankan untuk menghasilkan
output yang diinginkan pengguna. Perintah direpresentasikan sebagai instance
class Action
dan file direpresentasikan sebagai instance class
Artifact
. Diagram tersebut disusun dalam grafik asimetris, terarah, dan bipartit yang disebut
"grafik tindakan".
Artefak tersedia dalam dua jenis: artefak sumber (yang tersedia sebelum Bazel mulai dieksekusi) dan artefak turunan (yang perlu dibuat). Artefak turunan dapat terdiri dari beberapa jenis:
- **Artefak biasa. **Ini diperiksa apakah sudah yang terbaru dengan menghitung checksumnya, dengan mtime sebagai pintasan; kami tidak memeriksa file tersebut jika jamnya belum berubah.
- Artefak symlink yang belum diselesaikan. API ini diperiksa keaktualannya dengan memanggil readlink(). Tidak seperti artefak biasa, artefak ini dapat berupa symlink yang menjuntai. Biasanya digunakan dalam kasus ketika seseorang kemudian mengemas beberapa file ke dalam arsip.
- Artefak pohon. Ini bukan file tunggal, melainkan pohon direktori. Pemeriksaan ini
diupdate dengan memeriksa kumpulan file di dalamnya dan
kontennya. Class direpresentasikan sebagai
TreeArtifact
. - Artefak metadata yang konstan. Perubahan pada artefak ini tidak memicu build ulang. Ini digunakan secara eksklusif untuk informasi stempel build: kita tidak ingin melakukan build ulang hanya karena waktu saat ini berubah.
Tidak ada alasan mendasar mengapa artefak sumber tidak bisa menjadi artefak pohon atau
artefak symlink yang belum di-resolve, hanya saja kami belum mengimplementasikannya (meskipun kami
harus – mereferensikan direktori sumber dalam file BUILD
adalah salah satu
beberapa masalah kesalahan yang sudah lama ada di Bazel; kami memiliki
implementasi seperti itu yang diaktifkan oleh
properti JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1
)
Jenis Artifact
yang penting adalah perantara. Peristiwa tersebut ditunjukkan oleh instance
Artifact
yang merupakan output dari MiddlemanAction
. Aturan tersebut digunakan untuk
khusus menangani beberapa hal:
- Agregator gabungan digunakan untuk mengelompokkan artefak bersama-sama. Ini agar jika banyak tindakan menggunakan kumpulan input yang besar dan sama, kita tidak memiliki tepi dependensi N*M, hanya N+M (yang diganti dengan set bertingkat)
- Menjadwalkan perantara perantara memastikan bahwa tindakan berjalan sebelum tindakan lainnya.
Sebagian besar digunakan untuk analisis lint, tetapi juga untuk kompilasi C++ (lihat
CcCompilationContext.createMiddleman()
untuk penjelasan) - Perantara runfile digunakan untuk memastikan keberadaan hierarki runfile, sehingga satu tidak perlu bergantung secara terpisah pada manifes output dan setiap artefak tunggal yang dirujuk oleh hierarki runfiles.
Tindakan terbaik dipahami sebagai perintah yang perlu dijalankan, lingkungan yang dibutuhkan, dan kumpulan output yang dihasilkannya. Hal-hal berikut adalah komponen utama deskripsi tindakan:
- Command line yang perlu dijalankan
- Artefak input yang dibutuhkan
- Variabel lingkungan yang perlu ditetapkan
- Anotasi yang menjelaskan lingkungan (seperti platform) yang diperlukan untuk menjalankannya \
Ada juga beberapa kasus khusus lainnya, seperti menulis file yang kontennya diketahui oleh Bazel. Class ini adalah subclass dari AbstractAction
. Sebagian besar tindakannya adalah
SpawnAction
atau StarlarkAction
(yang sama, boleh dibilang bukan
class terpisah), meskipun Java dan C++ memiliki jenis tindakannya sendiri
(JavaCompileAction
, CppCompileAction
, dan CppLinkAction
).
Pada akhirnya, kami ingin memindahkan semuanya ke SpawnAction
; JavaCompileAction
cukup dekat, tetapi C++ sedikit mirip dengan kasus khusus karena penguraian file .d dan
menyertakan pemindaian.
Grafik tindakan sebagian besar "disematkan" ke dalam grafik Skyframe: secara konseptual, eksekusi tindakan direpresentasikan sebagai pemanggilan
ActionExecutionFunction
. Pemetaan dari tepi dependensi grafik tindakan ke
tepi dependensi Skyframe dijelaskan dalam
ActionExecutionFunction.getInputDeps()
dan Artifact.key()
serta memiliki beberapa pengoptimalan untuk menjaga agar jumlah tepi Skyframe tetap rendah:
- Artefak turunan tidak memiliki
SkyValue
-nya sendiri. Sebaliknya,Artifact.getGeneratingActionKey()
digunakan untuk mengetahui kunci tindakan yang menghasilkannya - 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 direktori yang ditentukan oleh konfigurasi dan paketnya (tetapi meskipun demikian, aturan dalam paket yang sama dapat bertentangan), tetapi aturan yang diterapkan di Java dapat menempatkan artefak turunan di mana saja.
Hal ini dianggap sebagai fitur yang salah, tetapi menghapusnya akan sangat sulit karena hal ini dapat menghemat waktu eksekusi secara signifikan jika, misalnya, file sumber perlu diproses sedemikian rupa dan file tersebut direferensikan oleh beberapa aturan (handwave-handwave). Ada beberapa RAM yang harus dibayar: 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. Hubungan kesetaraan 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 beberapa tempat di Bazel yang memerlukan tampilan "global" build tersebut.
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 yang menyatakan "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
),
opsi harus 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 hierarki direktori tunggal yang menyinkronkan
setiap paket dengan target yang digunakan dari lokasinya yang 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 entri jalur paket ke entri lainnya (digunakan untuk kejadian umum)
- Menghasilkan perintah yang berbeda jika tindakan dijalankan dari jarak jauh dibandingkan jika dijalankan secara lokal
- Pengujian ini memerlukan transformasi command line khusus untuk alat yang sedang digunakan (pertimbangkan perbedaan antara classpath Java dan jalur penyertaan C++)
- Mengubah command line dari tindakan akan membatalkan entri cache tindakannya
--package_path
perlahan tidak lagi digunakan
Kemudian, Bazel mulai melintasi grafik tindakan (grafik bipartit dan terarah
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 dicapai di balik Skyframe:
ActionExecutionFunction.stateMap
berisi data untuk membuat Skyframe dimulai ulang dariActionExecutionFunction
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 balik Skyframe; meskipun tindakan dijalankan ulang di Skyframe, tindakan tersebut masih dapat menjadi hit di cache tindakan lokal. Ini menunjukkan status sistem file lokal dan diserialisasi ke disk yang berarti bahwa saat seseorang memulai server Bazel baru, seseorang dapat memperoleh hit cache tindakan lokal meskipun grafik Skyframe kosong.
Cache ini diperiksa untuk hit menggunakan metode
ActionCacheChecker.getTokenIfNeedToExecute()
.
Berlawanan dengan namanya, ini adalah peta dari jalur artefak turunan ke tindakan yang memunculkannya. Tindakan tersebut dideskripsikan sebagai berikut:
- Kumpulan file input dan output serta checksumnya
- "Kunci tindakannya" biasanya berupa command line yang dieksekusi, tetapi secara umum mewakili semua yang tidak diambil oleh checksum file input (seperti untuk
FileWriteAction
, merupakan checksum data yang ditulis)
Ada juga "cache tindakan top-down" yang sangat eksperimental dan masih dalam pengembangan, yang menggunakan hash transitif agar tidak masuk ke cache berkali-kali.
Penemuan input dan pemangkasan input
Beberapa tindakan lebih rumit daripada hanya memiliki sekumpulan input. Perubahan pada set input tindakan terdiri dari dua bentuk:
- Suatu tindakan mungkin menemukan input baru sebelum eksekusinya, atau memutuskan bahwa beberapa
inputnya sebenarnya tidak diperlukan. Contoh kanonis adalah C++,
sebaiknya Anda membuat perkiraan matang tentang file header yang digunakan oleh file C++
dari penutupan transitifnya, sehingga kami tidak ingin mengirim setiap
file header ke eksekutor jarak jauh. Oleh karena itu, kami memiliki opsi untuk tidak mendaftarkan setiap
file header sebagai "input", tetapi memindai file sumber untuk
menyertakan header secara transitif dan hanya menandai file tersebut sebagai input
#include
- Suatu tindakan mungkin menyadari bahwa beberapa file tidak digunakan selama eksekusinya. Di C++, ini disebut "file .d": compiler memberi tahu file header yang digunakan setelah fakta, dan untuk menghindari rasa malu memiliki inkrementalitas yang lebih buruk daripada Make, Bazel memanfaatkan fakta ini. Opsi ini menawarkan perkiraan yang lebih baik daripada pemindai penyertaan karena mengandalkan compiler.
Hal ini diterapkan menggunakan metode pada Action:
Action.discoverInputs()
dipanggil. Tindakan ini akan menampilkan kumpulan Artefak bertingkat yang dianggap wajib. Keduanya harus merupakan artefak sumber, sehingga tidak ada tepi dependensi dalam grafik tindakan yang tidak memiliki setara dengan 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 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 tersebut mencerminkan hasil dari penemuan dan pemangkasan input yang dilakukan sebelumnya.
Tindakan Starlark dapat memanfaatkan fasilitas untuk mendeklarasikan beberapa input sebagai tidak
digunakan dengan argumen unused_inputs_list=
dari
ctx.actions.run()
.
Berbagai cara untuk menjalankan tindakan: Strategi/ActionContext
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 mewujudkannya disebut ActionContext
(atau Strategy
, karena kita
berhasil melakukan separuh jalan dengan mengganti nama...)
Siklus proses konteks tindakan adalah sebagai berikut:
- Saat fase eksekusi dimulai, instance
BlazeModule
akan ditanyai konteks tindakan apa yang dimilikinya. Ini terjadi pada konstruktorExecutionTool
. Jenis konteks tindakan diidentifikasi oleh instanceClass
Java yang mengacu pada sub-antarmukaActionContext
dan antarmuka mana yang harus diimplementasikan konteks tindakan. - Konteks tindakan yang sesuai dipilih dari yang tersedia dan
diteruskan ke
ActionExecutionContext
danBlazeExecutor
. - Tindakan meminta konteks menggunakan
ActionExecutionContext.getContext()
danBlazeExecutor.getStrategy()
(sebaiknya hanya ada satu cara untuk melakukannya...)
Strategi bebas memanggil strategi lain untuk melakukan tugasnya; strategi ini digunakan, misalnya, dalam strategi dinamis yang memulai tindakan baik secara lokal maupun jarak jauh, kemudian menggunakan penyelesaian mana pun terlebih dahulu.
Salah satu strategi penting adalah yang menerapkan proses pekerja persisten
(WorkerSpawnStrategy
). Idenya adalah bahwa beberapa alat memiliki waktu startup yang lama
dan oleh karena itu harus digunakan kembali di antara tindakan, bukan memulai yang baru untuk
setiap tindakan (ini menunjukkan potensi masalah ketepatan, karena Bazel
mengjanjikan proses pekerja yang tidak membawa status
yang dapat diamati antara permintaan individual)
Jika alat berubah, proses pekerja perlu dimulai ulang. Kemampuan pekerja
dapat digunakan kembali ditentukan dengan menghitung checksum untuk alat yang digunakan menggunakan
WorkerFilesHash
. Hal ini bergantung pada mengetahui input tindakan yang mewakili
bagian alat dan yang mewakili input; hal ini ditentukan oleh pembuat
Tindakan: Spawn.getToolFiles()
dan runfile Spawn
dihitung sebagai bagian dari alat tersebut.
Informasi selengkapnya tentang strategi (atau konteks tindakan):
- Informasi tentang berbagai strategi untuk menjalankan tindakan tersedia di sini.
- Informasi tentang strategi dinamis, tempat kami menjalankan tindakan secara lokal dan dari jarak jauh untuk melihat penyelesaian mana yang terlebih dahulu tersedia di sini.
- Informasi tentang seluk-beluk mengeksekusi 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 dari tindakan ke tindakan: semakin banyak resource yang diperlukan suatu tindakan, semakin sedikit instance yang harus dijalankan secara bersamaan untuk menghindari overload mesin lokal.
Hal ini diimplementasikan di class ResourceManager
: setiap tindakan harus
dianotasi dengan perkiraan resource lokal yang diperlukan dalam bentuk
instance ResourceSet
(CPU dan RAM). Selanjutnya, saat konteks tindakan melakukan sesuatu
yang memerlukan resource lokal, konteks tersebut 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 tempatnya mendapatkan output. 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 pada build yang sama, keduanya harus memiliki direktori berbeda sehingga keduanya bisa memiliki versi tindakan yang sama sendiri; jika tidak, jika kedua konfigurasi tersebut tidak sesuai seperti command line tindakan yang menghasilkan file output yang sama, Bazel tidak akan mengetahui tindakan mana yang harus dipilih ("konflik tindakan")
- Jika dua konfigurasi mewakili "kira-kira" sama, keduanya harus memiliki nama yang sama sehingga tindakan yang dieksekusi di satu konfigurasi dapat digunakan kembali untuk yang lain jika command line cocok: misalnya, perubahan pada opsi command line ke compiler Java tidak akan menyebabkan tindakan kompilasi C++ dijalankan ulang.
Sejauh ini, kami belum menemukan cara prinsip untuk menyelesaikan masalah ini, yang memiliki kesamaan dengan masalah pemangkasan konfigurasi. Diskusi opsi yang lebih panjang tersedia di sini. Area utama yang bermasalah adalah aturan Starlark (yang penulisnya biasanya tidak terlalu familier dengan Bazel) dan aspeknya, yang menambahkan dimensi lain ke ruang hal yang dapat menghasilkan file output "yang sama".
Pendekatan saat ini adalah segmen jalur untuk konfigurasi adalah
<CPU>-<compilation mode>
dengan berbagai akhiran yang ditambahkan sehingga transisi
konfigurasi yang diterapkan di Java tidak mengakibatkan konflik tindakan. Selain itu, checksum dari kumpulan transisi konfigurasi Starlark ditambahkan sehingga pengguna
tidak dapat menyebabkan konflik tindakan. Jauh dari keinginan Anda. Hal ini diimplementasikan di
OutputDirectories.buildMnemonic()
dan bergantung pada setiap fragmen konfigurasi
yang menambahkan bagiannya sendiri ke nama direktori output.
Pengujian
Bazel memiliki dukungan yang beragam untuk menjalankan pengujian. Kode ini mendukung:
- Menjalankan pengujian dari jarak jauh (jika backend eksekusi jarak jauh tersedia)
- Menjalankan pengujian beberapa kali secara paralel (untuk deflaking atau mengumpulkan data waktu)
- Sharding pengujian (membagi kasus pengujian dalam pengujian yang sama pada beberapa proses untuk mendapatkan kecepatan)
- Menjalankan kembali uji yang tidak stabil
- Mengelompokkan pengujian ke dalam rangkaian pengujian
Pengujian adalah target yang dikonfigurasi reguler dan memiliki TestProvider, yang menjelaskan cara menjalankan pengujian:
- Artefak yang bangunannya menghasilkan pengujian yang sedang dijalankan. Ini adalah file "cache
status" yang berisi pesan
TestResultData
berseri - Frekuensi pengujian harus dijalankan
- Jumlah shard yang akan digunakan untuk membagi pengujian
- Beberapa parameter tentang cara menjalankan pengujian (seperti waktu tunggu pengujian)
Menentukan pengujian yang akan dijalankan
Menentukan pengujian yang dijalankan adalah proses yang rumit.
Pertama, selama penguraian pola target, rangkaian pengujian akan diperluas secara rekursif. Ekspansi
diterapkan di TestsForTargetPatternFunction
. Kerugian
yang cukup mengejutkan adalah jika rangkaian pengujian mendeklarasikan tidak ada pengujian, merujuk pada
setiap pengujian dalam paketnya. Hal ini diimplementasikan di Package.beforeBuild()
dengan
menambahkan atribut implisit yang disebut $implicit_tests
ke aturan rangkaian pengujian.
Kemudian, pengujian difilter untuk ukuran, tag, waktu tunggu, dan bahasa sesuai dengan
opsi command line. Hal ini diterapkan di TestFilter
dan dipanggil dari
TargetPatternPhaseFunction.determineTests()
selama penguraian target dan
hasilnya akan dimasukkan ke dalam TargetPatternPhaseValue.getTestsToRunLabels()
. Alasan mengapa atribut aturan yang dapat difilter tidak dapat dikonfigurasi adalah bahwa hal ini terjadi sebelum fase analisis, sehingga konfigurasi tidak tersedia.
Kemudian, pemrosesan ini dilakukan lebih lanjut di BuildView.createResult()
: target yang
analisisnya gagal akan difilter dan pengujian dibagi menjadi pengujian eksklusif dan
tidak eksklusif. Kemudian, kode tersebut dimasukkan ke dalam AnalysisResult
, yang merupakan cara
ExecutionTool
mengetahui pengujian mana yang akan dijalankan.
Untuk memberikan transparansi pada proses yang rumit ini, operator kueri tests()
(diterapkan di TestsFunction
) tersedia untuk memberi tahu pengujian mana yang dijalankan saat target tertentu ditentukan pada command line. Sayangnya
ini merupakan penerapan ulang, sehingga mungkin menyimpang dari hal di atas
dalam beberapa hal yang sulit.
Menjalankan pengujian
Cara pengujian dijalankan adalah dengan meminta artefak status cache. Hal ini kemudian
menghasilkan eksekusi TestRunnerAction
, yang 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 darinya. Deskripsi mendetail tentang apa yang diharapkan Bazel dari pengujian dan apa yang dapat diharapkan dari Bazel tersedia di sini. Dalam cara paling sederhana, kode keluar 0 berarti berhasil, sedangkan yang lainnya berarti gagal.
Selain file status cache, setiap proses pengujian juga memunculkan sejumlah
file lainnya. File tersebut ditempatkan di "direktori log pengujian" yang merupakan subdirektori yang disebut
testlogs
dari direktori output konfigurasi target:
test.xml
, file XML bergaya JUnit yang memerinci 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 uji yang tidak dapat dilakukan selama pembuatan target reguler: eksekusi pengujian eksklusif dan streaming output.
Beberapa pengujian harus 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 biasa, yang output terminalnya dihapus saat tindakan
selesai, pengguna dapat meminta output pengujian untuk di-streaming sehingga mereka
diberi tahu tentang progres pengujian yang berjalan lama. Ini ditentukan oleh
opsi command line --test_output=streamed
dan menyiratkan eksekusi
pengujian eksklusif sehingga output dari berbagai pengujian tidak diselingi.
Hal ini diterapkan di class StreamedTestOutput
yang diberi nama dengan tepat dan berfungsi dengan memeriksa 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
).
Ini dibuang ke Protokol Peristiwa Build dan dihasilkan 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 uji digabungkan dalam skrip yang disebut
collect_coverage.sh
.
Skrip ini menyiapkan lingkungan pengujian untuk mengaktifkan pengumpulan cakupan dan menentukan tempat file cakupan ditulis oleh runtime cakupan. Android Studio kemudian menjalankan pengujian. Pengujian dapat berjalan sendiri di beberapa subproses dan terdiri dari bagian-bagian yang ditulis dalam beberapa bahasa pemrograman yang berbeda (dengan runtime koleksi cakupan terpisah). Skrip wrapper bertanggung jawab untuk mengonversi file yang dihasilkan ke format LCOV jika 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
dicapai 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 bahasa 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. Masalahnya dipecahkan adalah jika Anda
ingin menghitung cakupan pengujian untuk biner, Anda tidak dapat menggabungkan
cakupan semua pengujian karena mungkin ada kode dalam biner yang tidak
ditautkan ke pengujian apa pun. Oleh karena itu, yang kami lakukan adalah membuat file cakupan untuk setiap
biner yang hanya berisi file yang kami kumpulkan cakupannya tanpa baris
tercakup. File cakupan dasar pengukuran untuk target berada di
bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
. Kode ini juga dibuat
untuk biner dan library selain pengujian jika Anda meneruskan
flag --nobuild_tests_only
ke Bazel.
Cakupan dasar pengukuran saat ini rusak.
Kami melacak dua grup file untuk koleksi cakupan bagi setiap aturan: serangkaian file berinstrumen dan kumpulan file metadata instrumentasi.
Kumpulan file berinstrumen hanyalah serangkaian file ke instrumen. Untuk runtime cakupan online, ini dapat digunakan saat runtime untuk menentukan file mana yang akan diinstrumentasi. Hal ini juga digunakan untuk menerapkan cakupan dasar pengukuran.
Set file metadata instrumentasi adalah kumpulan file tambahan yang diperlukan pengujian untuk membuat file LCOV yang diperlukan Bazel dari file tersebut. Dalam praktiknya, hal ini terdiri dari file khusus runtime; misalnya, gcc memunculkan file .gcno selama kompilasi. Parameter ini ditambahkan ke kumpulan input tindakan pengujian jika mode cakupan diaktifkan.
Apakah cakupan sedang dikumpulkan atau tidak, data tersebut disimpan di
BuildConfiguration
. Hal ini berguna karena ini adalah cara mudah untuk mengubah tindakan pengujian
dan grafik tindakan bergantung pada bit ini, tetapi juga berarti bahwa jika
bit ini dibalik, semua target perlu dianalisis ulang (beberapa bahasa, seperti
C++, memerlukan opsi compiler yang berbeda untuk memunculkan kode yang dapat mengumpulkan cakupan,
yang sedikit mengurangi masalah ini, karena setelah itu analisis ulang tetap diperlukan).
File dukungan cakupan bergantung pada label dalam dependensi implisit sehingga dapat diganti dengan kebijakan pemanggilan, yang memungkinkan file tersebut berbeda di antara berbagai versi Bazel. Idealnya, perbedaan ini akan dihapus, dan kami menstandardisasi salah satunya.
Kami juga membuat "laporan cakupan" yang menggabungkan cakupan yang dikumpulkan untuk
setiap pengujian dalam pemanggilan Bazel. Ini ditangani oleh
CoverageReportActionFactory
dan dipanggil dari BuildView.createResult()
. Alat ini
mendapatkan akses ke alat yang diperlukan dengan melihat atribut :coverage_report_generator
dari pengujian pertama yang dijalankan.
Mesin kueri
Bazel memiliki bahasa kecil yang digunakan untuk menanyakan berbagai hal tentang berbagai grafik. Jenis kueri berikut disediakan:
bazel query
digunakan untuk menyelidiki grafik targetbazel cquery
digunakan untuk menyelidiki grafik target yang dikonfigurasibazel aquery
digunakan untuk menyelidiki grafik tindakan
Masing-masing diterapkan dengan membuat subclass AbstractBlazeQueryEnvironment
.
Fungsi kueri tambahan dapat dilakukan dengan membuat subclass QueryFunction
. Untuk mengizinkan hasil kueri streaming, daripada 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, serta class
aturan, XML, protobuf, dan sebagainya. Ini diimplementasikan sebagai subclass
OutputFormatter
.
Persyaratan kecil pada beberapa format output kueri (proto, tentu saja) adalah bahwa Bazel perlu memunculkan _semua _informasi yang disediakan paket agar dapat membedakan output dan menentukan apakah target tertentu telah berubah. Akibatnya, nilai atribut harus dapat diserialisasi, sehingga hanya ada sedikit jenis atribut tanpa atribut apa pun yang memiliki nilai Starlark yang kompleks. Solusi yang biasa digunakan adalah menggunakan label, dan melampirkan informasi kompleks ke aturan dengan label tersebut. Hal ini bukan solusi yang sangat memuaskan dan akan sangat membantu untuk mencabut persyaratan ini.
Sistem modul
Bazel dapat diperluas dengan menambahkan modul ke dalamnya. Setiap modul harus membuat subclass
BlazeModule
(namanya adalah histori histori Bazel saat digunakan
disebut Blaze) dan mendapatkan informasi tentang berbagai peristiwa selama eksekusi
perintah.
Umumnya sebagian besar digunakan untuk menerapkan berbagai fungsi "non-inti" yang hanya diperlukan beberapa versi Bazel (seperti yang kami gunakan di Google):
- Antarmuka ke sistem eksekusi jarak jauh
- Perintah baru
Serangkaian titik ekstensi yang ditawarkan BlazeModule
agak acak. Jangan
gunakan ini sebagai contoh prinsip desain yang baik.
Bus 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 sana, dan modul dapat mendaftarkan pemroses untuk peristiwa
yang mereka minati. Misalnya, hal berikut direpresentasikan sebagai peristiwa:
- Daftar target build yang akan di-build telah ditentukan
(
TargetParsingCompleteEvent
) - Konfigurasi level 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
(adalah BuildEvent
). Hal ini tidak hanya memungkinkan BlazeModule
, tetapi juga berbagai hal
di luar proses Bazel untuk mengamati build. Class ini dapat diakses sebagai
file yang berisi pesan protokol atau Bazel dapat terhubung ke server (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 didesain untuk digunakan dalam monorepo (satu hierarki sumber yang berisi semua hal yang diperlukan untuk mem-build), Bazel berada di dunia yang tidak sepenuhnya benar. "Repositori eksternal" adalah abstraksi yang digunakan untuk menjembatani kedua dunia ini: keduanya mewakili kode yang diperlukan untuk build, tetapi tidak berada di hierarki sumber utama.
File WORKSPACE
Kumpulan repositori eksternal ditentukan dengan menguraikan file WORKSPACE. Misalnya, deklarasi seperti ini:
local_repository(name="foo", path="/foo/bar")
Menghasilkan repositori bernama @foo
yang tersedia. Hal ini akan mempersulit orang untuk 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 potongan-potongan yang ditunjukkan oleh pernyataan
load()
. Indeks potongan ditunjukkan oleh WorkspaceFileKey.getIndex()
dan
menghitung WorkspaceFileFunction
hingga indeks X berarti mengevaluasinya hingga
pernyataan load()
ke-X.
Mengambil repositori
Sebelum kode repositori tersedia, Bazel harus
diambil. Tindakan ini akan menghasilkan Bazel membuat direktori pada
$OUTPUT_BASE/external/<repository name>
.
Pengambilan repositori terjadi pada langkah-langkah berikut:
PackageLookupFunction
menyadari bahwa diperlukan repositori dan membuatRepositoryName
sebagaiSkyKey
, yang memanggilRepositoryLoaderFunction
RepositoryLoaderFunction
meneruskan permintaan keRepositoryDelegatorFunction
karena alasan yang tidak jelas (kode mengatakan untuk menghindari mendownload ulang sesuatu jika Skyframe dimulai ulang, tetapi ini bukan alasan yang sangat kuat)RepositoryDelegatorFunction
menemukan aturan repositori yang diminta untuk diambil dengan melakukan iterasi pada potongan file WORKSPACE hingga repositori yang diminta ditemukan- Ditemukan
RepositoryFunction
yang sesuai yang mengimplementasikan pengambilan repositori; baik implementasi Starlark untuk repositori maupun peta hard code untuk repositori yang diimplementasikan di Java.
Ada berbagai lapisan cache karena pengambilan repositori dapat sangat mahal:
- Ada cache untuk file yang didownload yang dikunci oleh checksum (
RepositoryCache
). Hal ini mengharuskan checksum tersedia di file Workspace, tetapi tetap cocok untuk hermeticity. Dukungan ini digunakan bersama oleh setiap instance server Bazel di workstation yang sama, terlepas dari ruang kerja atau basis output yang digunakan. - "File penanda" ditulis untuk setiap repositori pada
$OUTPUT_BASE/external
yang berisi checksum aturan yang digunakan untuk mengambilnya. Jika server Bazel dimulai ulang, tetapi checksum tidak berubah, server tersebut 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, yang tidak mengharuskan Bazel mengambil data acak dari internet. Hal ini diterapkan olehDownloadManager
.
Setelah repositori didownload, artefak di dalamnya diperlakukan sebagai artefak
sumber. Hal ini menimbulkan masalah karena Bazel biasanya memeriksa
artefak sumber terbaru dengan memanggil stat() pada artefak tersebut, dan artefak ini juga
dibatalkan validasinya saat definisi repositori tempatnya berubah. Dengan demikian,
FileStateValue
untuk artefak di repositori eksternal harus bergantung pada
repositori eksternalnya. Hal ini ditangani oleh ExternalFilesHelper
.
Direktori terkelola
Terkadang, repositori eksternal perlu mengubah file pada root workspace (seperti pengelola paket yang menampung paket yang didownload di subdirektori hierarki sumber). Hal ini bertentangan dengan asumsi bahwa Bazel membuat file sumber hanya diubah oleh pengguna, bukan sendiri dan memungkinkan paket merujuk ke setiap direktori di bawah root workspace. Untuk membuat repositori eksternal seperti ini berfungsi, Bazel melakukan dua hal:
- Memungkinkan pengguna menentukan subdirektori ruang kerja yang tidak
dapat dijangkau oleh Bazel. Fungsi tersebut tercantum dalam file bernama
.bazelignore
dan fungsinya diimplementasikan diBlacklistedPackagePrefixesFunction
. - Kami mengenkode pemetaan dari subdirektori ruang kerja ke repositori
eksternal yang ditanganinya ke dalam
ManagedDirectoriesKnowledge
dan menanganiFileStateValue
yang merujuknya dengan cara yang sama seperti repositori eksternal reguler.
Pemetaan repositori
Hal ini dapat terjadi karena beberapa repositori ingin bergantung pada repositori yang sama,
tetapi dalam versi yang berbeda (ini adalah instance dari "masalah dependensi
diamond"). Misalnya, jika dua biner dalam repositori terpisah dalam build
ingin bergantung pada Guava, keduanya mungkin merujuk ke Guava dengan label
yang memulai @guava//
dan itu berarti versi yang berbeda.
Oleh karena itu, Bazel memungkinkan salah satu untuk memetakan ulang label repositori eksternal sehingga
string @guava//
dapat merujuk ke satu repositori Guava (seperti @guava1//
) di
repositori dari satu biner dan repositori Guava lainnya (seperti @guava2//
)
repositori dari yang lain.
Atau, ini juga dapat digunakan untuk bergabung dengan diamond. Jika repositori
bergantung pada @guava1//
, dan repositori lainnya bergantung pada @guava2//
, pemetaan repositori
memungkinkan Anda memetakan ulang kedua repositori untuk menggunakan repositori @guava//
kanonis.
Pemetaan ditentukan dalam file WORKSPACE sebagai atribut repo_mapping
dari definisi repositori individual. Kemudian, pesan tersebut akan muncul di Skyframe sebagai anggota
WorkspaceFileValue
, yang ditambahkan ke:
Package.Builder.repositoryMapping
yang digunakan untuk mengubah atribut aturan bernilai label dalam paket olehRuleClass.populateRuleAttributeValues()
Package.repositoryMapping
yang digunakan dalam fase analisis (untuk menyelesaikan hal-hal seperti$(location)
yang tidak diurai dalam fase pemuatan)BzlLoadFunction
untuk menyelesaikan label dalam pernyataan load()
Bit JNI
Server Bazel__ sebagian besar _ditulis dalam Java. Pengecualiannya adalah bagian-bagian yang tidak dapat dilakukan sendiri oleh Java 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 pada src/main/native dan class Java dengan metode native adalah:
NativePosixFiles
danNativePosixFileSystem
ProcessUtils
WindowsFileOperations
danWindowsFileProcesses
com.google.devtools.build.lib.platform
Output konsol
Memberikan output konsol tampak sederhana, tetapi pertemuan menjalankan beberapa proses (terkadang dari jarak jauh), caching yang sangat mendetail, keinginan untuk memiliki output terminal yang bagus dan penuh warna, serta memiliki server yang berjalan lama membuatnya tidak mudah.
Tepat setelah panggilan RPC masuk dari klien, dua instance RpcOutputStream
dibuat (untuk stdout dan stderr) yang meneruskan data yang dicetak ke
klien. Kemudian, nilai tersebut digabungkan dalam pasangan OutErr
(stdout, stderr). Apa pun yang perlu dicetak di konsol akan melalui
streaming ini. Kemudian, aliran ini diserahkan ke
BlazeCommandDispatcher.execExclusively()
.
Output secara default dicetak dengan urutan escape ANSI. Jika parameter ini tidak
diinginkan (--color=no
), permintaan tersebut akan dihapus oleh AnsiStrippingOutputStream
. Selain itu, System.out
dan System.err
dialihkan ke aliran output ini.
Hal ini dilakukan agar informasi proses debug dapat dicetak menggunakan
System.err.println()
dan masih berada dalam output terminal klien
(yang berbeda dengan server). Perlu diperhatikan bahwa jika suatu proses
menghasilkan output biner (seperti bazel query --output=proto
), tidak ada pengurangan stdout
yang terjadi.
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 mereka mungkin memiliki Location
(tempat dalam kode sumber yang menyebabkan peristiwa terjadi).
Beberapa implementasi EventHandler
menyimpan peristiwa yang diterima. Ini digunakan
untuk memutar ulang informasi ke UI yang disebabkan oleh berbagai jenis pemrosesan yang di-cache,
misalnya peringatan yang dikeluarkan oleh target yang dikonfigurasi dan di-cache.
Beberapa EventHandler
juga memungkinkan postingan peristiwa yang akhirnya sampai ke bus peristiwa (Event
biasa _tidak _muncul di sana). Ini adalah implementasi ExtendedEventHandler
dan kegunaan utamanya adalah untuk memutar ulang peristiwa EventBus
yang di-cache. Semua peristiwa EventBus
ini mengimplementasikan Postable
, tetapi tidak
semua yang diposting ke EventBus
tentunya harus mengimplementasikan antarmuka ini;
hanya peristiwa yang di-cache oleh ExtendedEventHandler
(akan menyenangkan dan
sebagian besar hal tersebut dilakukan; tetapi tidak diterapkan)
Output terminal sebagian besar dikeluarkan melalui UiEventHandler
, yang
bertanggung jawab atas semua pemformatan output yang elegan dan pelaporan progres
yang dilakukan Bazel. Laporan ini memiliki dua input:
- Bus acara
- Streaming acara yang ditransfer ke dalamnya melalui Pelapor
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 ini. Ini hanya digunakan ketika perintah perlu membuang sejumlah besar data biner yang mungkin (seperti bazel query
).
Membuat Profil Bazel
Bazel cepat. Bazel juga lambat, karena build cenderung tumbuh hingga
hanya bagian dari apa yang dapat dihasilkan. Karena alasan ini, Bazel menyertakan profiler yang dapat
digunakan untuk build profil dan Bazel itu sendiri. Hal ini diterapkan di class yang
bernama Profiler
. Metode ini diaktifkan secara default, meskipun hanya mencatat
data singkat sehingga overhead-nya dapat ditoleransi; Command line
--record_full_profiler_data
membuatnya merekam semua yang dapat dilakukan.
Fitur ini membuat profil dalam format profiler Chrome; profil ini paling baik dilihat di Chrome. Model datanya adalah stack tugas: Anda dapat memulai tugas dan mengakhiri tugas, serta harus disusun bertingkat dengan rapi. Setiap thread Java mendapatkan tumpukan tugasnya sendiri. TODO: Bagaimana cara kerjanya dengan gaya dan penerusan kelanjutan?
Profiler dimulai dan dihentikan masing-masing di BlazeRuntime.initProfiler()
dan
BlazeRuntime.afterCommand()
, serta berupaya untuk aktif selama
mungkin sehingga kita dapat membuat profil semuanya. Untuk menambahkan sesuatu ke profil,
panggil Profiler.instance().profile()
. Fungsi ini menampilkan Closeable
, yang penutupannya
menunjukkan akhir tugas. Sebaiknya gunakan dengan pernyataan try-with-resources.
Kami juga melakukan pembuatan profil memori dasar pada MemoryProfiler
. Layanan ini juga selalu aktif
dan sebagian besar merekam ukuran heap maksimum dan perilaku GC.
Menguji Bazel
Bazel memiliki dua jenis pengujian utama: pengujian yang menganggap Bazel sebagai "kotak hitam" dan pengujian yang hanya menjalankan fase analisis. Kami menyebut pengujian pertama sebagai "pengujian integrasi" dan "pengujian unit" terakhir, meskipun lebih seperti pengujian integrasi yang tidak terlalu terintegrasi. Kami juga memiliki beberapa pengujian unit sebenarnya, jika diperlukan.
Ada dua jenis pengujian integrasi:
- Yang diimplementasikan menggunakan framework pengujian bash yang sangat rumit pada
src/test/shell
- Yang diimplementasikan di Java. Ini diimplementasikan sebagai subclass
BuildIntegrationTestCase
BuildIntegrationTestCase
adalah framework pengujian integrasi yang lebih disukai 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 gosok 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.