Dokumen ini berisi deskripsi tentang code base dan struktur Bazel. Ini ditujukan bagi orang-orang yang bersedia berkontribusi pada Bazel, bukan untuk pengguna akhir.
Pengantar
Basis kode Bazel besar (~ kode produksi 350KLOC dan uji KLOC ~260 KLOC kode) dan tidak ada yang mengetahui keseluruhan lanskap: semua orang tahu lembah tertentu dengan sangat baik, tetapi hanya sedikit yang tahu apa yang ada di atas bukit di setiap arah.
Agar orang-orang di tengah perjalanan tidak menemukan diri mereka dalam hutan gelap dengan jalur langsung yang hilang, dokumen ini mencoba untuk memberikan ikhtisar tentang {i>code base<i} sehingga lebih mudah untuk memulai yang sedang mengerjakannya.
Versi publik kode sumber Bazel terdapat pada GitHub di github.com/bazelbuild/bazel. Ini bukan “sumber kebenaran”; data itu berasal dari hierarki sumber internal Google yang berisi fungsi tambahan yang tidak berguna di luar Google. Tujuan tujuan jangka panjangnya adalah menjadikan GitHub sebagai sumber kebenaran.
Kontribusi diterima melalui mekanisme permintaan pull GitHub biasa, dan diimpor secara manual oleh seorang Googler ke dalam hierarki sumber internal, lalu diekspor ulang ke GitHub.
Arsitektur klien/server
Sebagian besar Bazel berada dalam proses server yang tetap berada di RAM di antara build. Hal ini memungkinkan Bazel mempertahankan status di antara build.
Inilah mengapa baris perintah Bazel memiliki dua jenis opsi: {i>startup<i} dan perintah. Dalam command line seperti ini:
bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar
Beberapa opsi (--host_jvm_args=
) ditempatkan sebelum nama perintah yang akan dijalankan
dan beberapa setelah (-c opt
); jenis yang pertama
disebut “opsi {i>startup<i}” dan
mempengaruhi proses server secara keseluruhan, sedangkan jenis yang terakhir, perintah
"opsi", hanya memengaruhi satu perintah.
Setiap instance server memiliki satu hierarki sumber yang terkait ("ruang kerja") dan masing-masing ruang kerja biasanya memiliki satu instance server aktif. Hal ini dapat dihindari dengan menentukan basis output kustom (lihat bagian "Tata letak direktori" untuk informasi selengkapnya tertentu).
Bazel didistribusikan sebagai satu file ELF yang dapat dieksekusi yang juga merupakan file .zip yang valid.
Saat Anda mengetik bazel
, file ELF yang dapat dieksekusi di atas diimplementasikan dalam C++ (
"klien") mendapatkan kontrol. Klien menyiapkan proses server yang sesuai dengan menggunakan
langkah-langkah berikut:
- Memeriksa apakah aplikasi telah mengekstrak dirinya sendiri. Jika tidak, sistem akan melakukannya. Ini adalah asal implementasi server.
- Memeriksa apakah ada {i>instance<i} server aktif yang
berfungsi: sedang berjalan,
memiliki opsi startup yang tepat dan menggunakan direktori ruang kerja yang tepat. Ini
menemukan server yang sedang berjalan dengan melihat direktori
$OUTPUT_BASE/server
di mana ada file kunci dengan porta yang didengarkan oleh server. - Jika perlu, menghentikan proses server lama
- Jika diperlukan, mulai proses server baru
Setelah proses server yang sesuai sudah siap, perintah yang perlu dijalankan adalah
dikomunikasikan kepadanya melalui antarmuka gRPC, maka {i>output<i} Bazel disalurkan kembali
ke terminal. Hanya satu perintah yang dapat dijalankan secara bersamaan. Ini adalah
diimplementasikan menggunakan mekanisme penguncian yang rumit
dengan bagian-bagian di C++ dan bagian-bagian
Java. Ada beberapa infrastruktur untuk
menjalankan beberapa perintah secara paralel,
karena ketidakmampuan untuk menjalankan bazel version
secara paralel dengan perintah lain
agak memalukan. Penghalang utama adalah siklus proses BlazeModule
dan beberapa status di BlazeRuntime
.
Pada akhir suatu perintah, server Bazel
mengirimkan kode keluar klien
akan ditampilkan. Masalah yang menarik adalah implementasi bazel run
:
perintah ini adalah menjalankan sesuatu yang baru saja
dibangun Bazel, tetapi ia tidak dapat melakukannya
dari proses server karena
tidak memiliki terminal. Jadi, sebaliknya, memberitahu
klien biner apa yang seharusnya {i>ujexec()<i} dan dengan argumen apa.
Saat seseorang menekan Ctrl-C, klien akan menerjemahkannya menjadi panggilan Cancel di gRPC {i>fiber<i}, yang mencoba menghentikan perintah tersebut sesegera mungkin. Setelah Ctrl-C ketiga, klien mengirimkan SIGKILL ke server sebagai gantinya.
Kode sumber klien berada di bawah src/main/cpp
dan protokol yang digunakan untuk
berkomunikasi dengan server berada di src/main/protobuf/command_server.proto
.
Titik entri utama server adalah BlazeRuntime.main()
dan gRPC memanggil
dari klien akan ditangani oleh GrpcServerImpl.run()
.
Tata letak direktori
Bazel membuat serangkaian direktori yang agak rumit selama proses build. Lengkap tersedia di Tata letak direktori output.
"Ruang kerja" adalah pohon sumber yang menjalankan Bazel. Biasanya sesuai dengan sesuatu yang Anda periksa dari {i>source control<i}.
Bazel meletakkan semua datanya di bawah "root pengguna {i>output<i}". Hal ini biasanya
$HOME/.cache/bazel/_bazel_${USER}
, tetapi dapat diganti menggunakan
Opsi startup --output_user_root
.
"Basis penginstalan" adalah tempat Bazel diekstrak. Hal ini otomatis dilakukan
dan setiap versi Bazel mendapat subdirektori berdasarkan {i>checksum<i} di bawah
pada basis penginstalan. berada di $OUTPUT_USER_ROOT/install
secara default dan dapat diubah
menggunakan opsi command line --install_base
.
"Basis output" tempat instance Bazel terhubung ke
ke ruang kerja penerima. Setiap basis output memiliki maksimal satu instance server Bazel
yang berjalan kapan saja. Biasanya pukul $OUTPUT_USER_ROOT/<checksum of the path
to the workspace>
. Ini dapat diubah menggunakan
opsi mulai --output_base
,
yaitu, antara lain, berguna untuk
mengatasi keterbatasan yang hanya
satu instance Bazel dapat berjalan di
ruang kerja mana pun pada waktu tertentu.
Direktori output berisi, antara lain:
- Repositori eksternal yang diambil di
$OUTPUT_BASE/external
. - {i>Root<i} {i>exec<i}, sebuah direktori yang
berisi {i>symlink<i} ke semua sumber
kode untuk build saat ini. Lokasinya di
$OUTPUT_BASE/execroot
. Selama build, direktori kerjanya adalah$EXECROOT/<name of main repository>
. Kami berencana mengubah setelan ini menjadi$EXECROOT
, meskipun rencana jangka panjang karena ini adalah perubahan yang sangat tidak kompatibel. - File yang dibuat selama build.
Proses mengeksekusi perintah
Setelah server Bazel mendapatkan kontrol dan diberi tahu tentang perintah yang perlu eksekusi, urutan peristiwa berikut akan terjadi:
BlazeCommandDispatcher
akan diberi tahu tentang permintaan baru tersebut. Hal itu memutuskan apakah perintah membutuhkan ruang kerja untuk dijalankan (hampir setiap perintah kecuali yang tidak ada hubungannya dengan kode sumber, seperti versi atau help) dan apakah perintah lain sedang berjalan.Perintah yang tepat ditemukan. Setiap perintah harus mengimplementasikan antarmuka
BlazeCommand
dan harus memiliki anotasi@Command
(ini sedikit antipola, akan lebih baik jika semua {i>metadata<i} yang dibutuhkan perintah dijelaskan oleh metode padaBlazeCommand
)Opsi command line akan diuraikan. Setiap perintah memiliki command line yang berbeda , yang dijelaskan dalam anotasi
@Command
.Bus peristiwa dibuat. Bus peristiwa adalah aliran untuk peristiwa yang terjadi selama proses build. Beberapa di antaranya diekspor ke luar Bazel di bawah aegis Build Event Protocol untuk memberi tahu dunia tentang bagaimana berjalan.
Perintah mendapatkan kontrol. Perintah yang paling menarik adalah yang menjalankan sebuah build: build, uji, jalankan, cakupan, dan sebagainya: fungsi ini yang diimplementasikan oleh
BuildTool
.Kumpulan pola target pada baris perintah diurai dan menggunakan karakter pengganti seperti
//pkg:all
dan//pkg/...
telah diselesaikan. Hal ini diimplementasikan diAnalysisPhaseRunner.evaluateTargetPatterns()
dan ditetapkan ulang di Skyframe sebagaiTargetPatternPhaseValue
.Fase pemuatan/analisis dijalankan untuk menghasilkan grafik tindakan (arah grafik perintah asiklik yang perlu dieksekusi untuk build).
Fase eksekusi dijalankan. Ini berarti menjalankan setiap tindakan yang diperlukan untuk membangun target tingkat atas yang diminta akan dijalankan.
Opsi command line
Opsi baris perintah untuk pemanggilan Bazel dijelaskan dalam
Objek OptionsParsingResult
, yang kemudian berisi peta dari "option
kelas" pada nilai-nilai opsi. "Class opsi" adalah subclass dari
OptionsBase
dan mengelompokkan opsi command line yang berkaitan dengan masing-masing
lainnya. Contoh:
- Opsi yang terkait dengan bahasa pemrograman (
CppOptions
atauJavaOptions
). Class ini harus berupa subclassFragmentOptions
dan pada akhirnya digabungkan menjadi 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 C++ menyertakan pemindaian atau tidak) sudah dibaca
di fase eksekusi, tetapi hal itu selalu
membutuhkan pipa eksplisit karena
BuildConfiguration
tidak tersedia pada saat itu. Untuk informasi selengkapnya, lihat
bagian “Konfigurasi”.
PERINGATAN: Kita ingin berpura-pura bahwa instance OptionsBase
tidak dapat diubah dan
menggunakannya seperti itu (misalnya sebagai bagian dari SkyKeys
). Tidak seperti itu dan
memodifikasinya adalah cara yang sangat bagus
untuk menghancurkan Bazel dengan cara halus yang sulit
untuk men-debug. Sayangnya, membuatnya benar-benar tidak dapat diubah adalah upaya besar.
(Mengubah FragmentOptions
segera setelah dibuat sebelum orang lain
mendapatkan kesempatan untuk menyimpan referensi ke nama tersebut dan sebelum equals()
atau hashCode()
dipanggil.)
Bazel mempelajari class opsi dengan cara berikut:
- Beberapa di antaranya telah dihubungkan dengan Bazel (
CommonCommandOptions
) - Dari anotasi @Command pada setiap perintah Bazel
- Dari
ConfiguredRuleClassProvider
(ini adalah opsi command line yang terkait ke bahasa pemrograman individu) - Aturan Starlark juga dapat menentukan opsi mereka sendiri (lihat di sini)
Setiap opsi (tidak termasuk opsi yang ditentukan Starlark) adalah variabel anggota dari
Subclass FragmentOptions
yang memiliki anotasi @Option
, yang menentukan
nama dan jenis opsi baris perintah
beserta beberapa teks bantuan.
Jenis Java dari nilai opsi baris perintah
biasanya merupakan sesuatu yang sederhana
(string, bilangan bulat, Boolean, label, dll.). Namun, kami juga mendukung
opsi jenis yang lebih rumit; dalam hal ini, tugas mengonversi dari
{i>string <i}baris perintah ke tipe data akan
diturunkan ke implementasi dari
com.google.devtools.common.options.Converter
.
Pohon sumber, seperti yang terlihat oleh Bazel
Bazel terlibat dalam bisnis membangun perangkat lunak, yang terjadi dengan membaca dan menafsirkan kode sumber. Totalitas kode sumber yang dioperasikan Bazel disebut "ruang kerja" dan ia disusun ke dalam repositori, paket, dan aturan.
Repositori
"Repositori" merupakan pohon sumber tempat pengembang bekerja; hal tersebut biasanya mewakili satu proyek. Leluhur Bazel, Blaze, mengoperasikan komputer monorepo, yaitu, satu pohon sumber yang berisi semua kode sumber yang digunakan untuk menjalankan build. Sebaliknya, Bazel mendukung proyek yang kode sumbernya mencakup repositori tambahan. Repositori tempat Bazel dipanggil disebut "main repositori", yang lainnya disebut "repositori eksternal".
Repositori ditandai dengan file bernama WORKSPACE
(atau WORKSPACE.bazel
) di
direktori {i>root <i}itu. File ini berisi informasi yang bersifat "global" secara keseluruhan
membangun, misalnya, seperangkat
repositori eksternal yang tersedia. Alat ini berfungsi seperti
file Starlark biasa yang berarti bahwa seseorang
dapat load()
file Starlark lainnya.
Ini biasanya digunakan untuk menarik repositori
yang dibutuhkan oleh repositori
yang secara eksplisit direferensikan (kami menyebutnya "pola deps.bzl
")
Kode repositori eksternal di-symlink atau diunduh di
$OUTPUT_BASE/external
.
Saat menjalankan build, seluruh hierarki sumber perlu disatukan; ingin
dilakukan oleh SymlinkForest, yang menghubungkan setiap paket di repositori utama
$EXECROOT
dan setiap repositori eksternal ke $EXECROOT/external
atau
$EXECROOT/..
(yang pertama tentu saja membuat Anda tidak dapat memiliki paket
yang disebut external
di repositori utama; itu sebabnya kami bermigrasi dari
itu)
Paket
Setiap repositori terdiri dari paket, kumpulan file terkait dan
spesifikasi dependensi. Ini ditentukan oleh
file yang disebut
BUILD
atau BUILD.bazel
. Jika keduanya ada, Bazel akan lebih memilih BUILD.bazel
; alasan
alasan file BUILD
masih diterima adalah karena nenek moyang Bazel, Blaze, menggunakan
nama file. Namun, ternyata segmen jalur ini umum digunakan, terutama
di Windows, di mana nama file
tidak peka huruf besar/kecil.
Paket tidak saling bergantung: perubahan pada file BUILD
paket
tidak dapat menyebabkan
paket lain berubah. Penambahan atau penghapusan BUILD
file
_can _change paket lain, karena glob rekursif berhenti di batas paket
dan dengan demikian, keberadaan file BUILD
akan menghentikan rekursi.
Evaluasi file BUILD
disebut "pemuatan paket". Diimplementasikan
di class PackageFactory
, bekerja dengan memanggil penafsir Starlark dan
memerlukan pengetahuan tentang kumpulan class aturan yang tersedia. Hasil paket
pemuatan adalah objek Package
. Umumnya, ini adalah peta dari string (nama dari
target) ke target itu sendiri.
Sebagian besar kompleksitas selama pemuatan paket adalah globbing: Bazel tidak
mengharuskan setiap file sumber dicantumkan secara eksplisit dan sebagai gantinya dapat menjalankan glob
(misalnya glob(["**/*.java"])
). Tidak seperti {i>shell<i}, ia mendukung
glob rekursif yang
turun ke subdirektori (tetapi bukan ke dalam sub-paket). Hal ini memerlukan akses ke
sistem file dan karena itu mungkin lambat, kami
menerapkan segala macam trik untuk
membuatnya berjalan secara paralel
dan seefisien mungkin.
Globbing diimplementasikan di class berikut:
LegacyGlobber
, globber yang tidak sadar Skyframe dan cepatSkyframeHybridGlobber
, versi yang menggunakan Skyframe dan kembali ke globber lama untuk menghindari "Mulai ulang Skyframe" (dijelaskan di bawah)
Class Package
sendiri berisi beberapa anggota yang digunakan secara eksklusif untuk
mengurai file WORKSPACE dan yang tidak masuk akal untuk paket nyata. Ini adalah
cacat desain karena objek yang
menjelaskan paket biasa tidak boleh mengandung
yang mendeskripsikan hal lain. Ini mencakup:
- Pemetaan repositori
- Toolchain terdaftar
- Platform eksekusi yang terdaftar
Idealnya, akan ada lebih banyak pemisahan antara penguraian file WORKSPACE dari
menguraikan paket reguler sehingga Package
tidak perlu memenuhi kebutuhan
dari keduanya. Sayangnya ini sulit dilakukan karena keduanya terkait
secara mendalam.
Label, Target, dan Aturan
Paket terdiri dari target yang memiliki jenis berikut:
- File: hal-hal yang merupakan input atau output build. Di beberapa Dalam istilah Bazel, kami menyebutnya artefak (dibahas di bagian lain). Tidak semua file yang dibuat selama build adalah target; adalah hal biasa untuk {i>output<i} Bazel tidak boleh memiliki label terkait.
- Aturan: ini menjelaskan langkah-langkah untuk memperoleh output-nya dari inputnya. Mereka
umumnya terkait dengan bahasa pemrograman (seperti
cc_library
,java_library
ataupy_library
), tetapi ada beberapa bahasa tanpa bahasa (sepertigenrule
ataufilegroup
) - Grup paket: dibahas di bagian Visibilitas.
Nama target disebut Label. {i>Syntax<i} label adalah
@repo//pac/kage:name
, dengan repo
adalah nama repositori tempat Label
di dalam, pac/kage
adalah direktori tempat file BUILD
-nya berada dan name
adalah jalur
file (jika label mengacu pada file sumber) secara relatif terhadap direktori dari
paket. Saat merujuk ke target pada baris perintah, ada beberapa bagian label
dapat dihilangkan:
- Jika repositori dihilangkan, label akan ditempatkan di file utama repositori resource.
- Jika bagian paket dihilangkan (seperti
name
atau:name
), label akan digunakan berada dalam paket direktori kerja saat ini (jalur relatif berisi referensi tingkat tinggi (..) tidak diizinkan)
Semacam aturan (seperti "library C++") disebut "class aturan". Class aturan dapat
diimplementasikan baik di Starlark (fungsi rule()
) atau di Java (disebut
“aturan native”, ketik RuleClass
). Dalam jangka panjang, setiap bahasa
akan diterapkan di Starlark, tetapi beberapa kelompok aturan lama (seperti Java
atau C++) masih dalam Java untuk saat ini.
Class aturan Starlark perlu diimpor di awal BUILD
file
menggunakan pernyataan load()
, sedangkan class aturan Java adalah "bawaan" dikenal oleh
Bazel, karena telah terdaftar di ConfiguredRuleClassProvider
.
Class aturan berisi informasi seperti:
- Atributnya (seperti
srcs
,deps
): jenisnya, nilai default, kendala, dll. - Transisi konfigurasi dan aspek yang dilampirkan ke setiap atribut, jika ada
- Penerapan aturan
- Penyedia info transitif aturan "biasanya" membuat
Catatan terminologi: Dalam code base, kami sering menggunakan “Aturan” yang diartikan sebagai target
yang dibuat oleh class aturan. Dalam Starlark dan
dalam dokumentasi yang menghadap pengguna,
“Aturan” harus digunakan secara eksklusif untuk merujuk ke class aturan itu sendiri; target
hanyalah “target”. Perhatikan juga bahwa meskipun RuleClass
memiliki “class” dalam
tidak ada hubungan pewarisan Java antara kelas aturan dan target
dari jenis tersebut.
Rangka Langit
Kerangka kerja evaluasi yang mendasari Bazel disebut Skyframe. Modelnya adalah semua yang perlu dibangun selama pembangunan diatur ke dalam sebuah grafik asiklik dengan ujung-ujung yang menunjuk dari bagian data apa pun ke dependensinya, yaitu, potongan data lain yang perlu diketahui untuk menyusunnya.
Node dalam grafik disebut SkyValue
, dan namanya disebut
SkyKey
dtk. Keduanya sangat tidak dapat diubah; hanya objek yang tidak
dapat diubah yang harus
dapat dijangkau dari sana. Invarian ini hampir selalu berlaku, dan jika tidak
(seperti untuk class opsi individu BuildOptions
, yang merupakan anggota dari
BuildConfigurationValue
dan SkyKey
-nya) kami berusaha keras untuk tidak mengubahnya
atau untuk mengubahnya hanya dengan
cara yang tidak dapat diamati dari luar.
Selanjutnya, semua yang dihitung dalam Skyframe (seperti
target yang dikonfigurasi) juga harus tidak dapat diubah.
Cara paling mudah untuk mengamati grafik Skyframe adalah dengan menjalankan bazel dump
--skyframe=detailed
, yang menyimpan grafik, satu SkyValue
per baris. Yang terbaik
melakukannya untuk build kecil, karena
bisa menjadi cukup besar.
Skyframe tersedia dalam paket com.google.devtools.build.skyframe
. Tujuan
paket com.google.devtools.build.lib.skyframe
dengan nama serupa berisi
Bazel di atas Skyframe. Informasi selengkapnya tentang Skyframe dapat
tersedia di sini.
Untuk mengevaluasi SkyKey
tertentu menjadi SkyValue
, Skyframe akan memanggil metode
SkyFunction
yang sesuai dengan jenis kunci. Selama
evaluasi, sistem dapat meminta dependensi lain dari Skyframe dengan memanggil
berbagai overload dari SkyFunction.Environment.getValue()
. Opsi ini memiliki
efek samping pendaftaran dependensi tersebut ke dalam grafik internal Skyframe,
yang diketahui Skyframe untuk mengevaluasi
kembali fungsi ketika salah satu dependensinya
berubah. Dengan kata lain, {i>caching<i} dan
komputasi inkremental Skyframe bekerja di
perincian SkyFunction
dan SkyValue
.
Setiap kali SkyFunction
meminta dependensi yang tidak tersedia, getValue()
akan mengembalikan nol. Fungsi ini kemudian akan mengembalikan
kontrol ke Skyframe dengan
itu sendiri mengembalikan nol. Di lain waktu, Skyframe akan mengevaluasi
dependensi yang tidak tersedia, lalu mulai ulang fungsi dari awal — hanya
kali panggilan getValue()
akan berhasil dengan hasil non-null.
Konsekuensi dari hal ini adalah setiap komputasi yang dilakukan di dalam SkyFunction
sebelum {i>restart <i}harus diulangi. Tapi ini tidak termasuk pekerjaan yang dilakukan untuk
mengevaluasi dependensi SkyValues
, yang di-cache. Oleh karena itu, kita umumnya mengerjakan
terkait masalah ini dengan:
- Mendeklarasikan dependensi dalam batch (dengan menggunakan
getValuesAndExceptions()
) untuk membatasi jumlah {i>restart<i}. - Memecah
SkyValue
menjadi bagian-bagian terpisah yang dikomputasi oleh berbagaiSkyFunction
, sehingga dapat dikomputasi dan di-cache secara terpisah. Ini harus dilakukan secara strategis, karena memiliki potensi untuk meningkatkan memori tingkat penggunaan. - Menyimpan status di antara mulai ulang, baik menggunakan
SkyFunction.Environment.getState()
, atau mempertahankan cache statis ad hoc "di belakang Skyframe".
Pada dasarnya, kami memerlukan jenis solusi ini karena kami secara rutin memiliki ratusan ribu {i>node<i} Skyframe yang sedang beroperasi, dan Java tidak mendukung yang ringan.
Starlark
Starlark adalah bahasa khusus domain yang digunakan orang untuk mengkonfigurasi dan memperluas Bazel. Ia dipahami sebagai {i>subset<i} terbatas dari Python yang memiliki tipe yang jauh lebih sedikit, lebih banyak batasan pada alur kontrol, dan yang paling penting, ketetapan yang kuat jaminan untuk memungkinkan pembacaan serentak. Ini belum menyelesaikan Turing, mencegah beberapa (tetapi tidak semua) pengguna untuk mencoba mencapai dari tugas pemrograman dalam bahasa tersebut.
Starlark diimplementasikan dalam paket net.starlark.java
.
Alat ini juga memiliki implementasi Go independen
di sini. Java
implementasi yang digunakan dalam Bazel saat
ini adalah penerjemah.
Starlark digunakan dalam beberapa konteks, antara lain:
- Bahasa
BUILD
. Di sinilah aturan baru ditentukan. Kode Starlark yang berjalan dalam konteks ini hanya memiliki akses ke konten fileBUILD
itu sendiri dan file.bzl
yang dimuat olehnya. - Definisi aturan. Ini adalah cara aturan baru (seperti dukungan untuk bahasa) ditentukan. Kode Starlark yang berjalan dalam konteks ini memiliki akses ke konfigurasi dan data yang disediakan oleh dependensi langsungnya (lebih lanjut tentang nanti).
- File WORKSPACE. Di sinilah repositori eksternal (kode yang tidak dalam hierarki sumber utama) ditentukan.
- Definisi aturan repositori. Di sinilah jenis repositori eksternal baru ditentukan. Kode Starlark yang berjalan dalam konteks ini dapat menjalankan kode arbitrer di komputer tempat Bazel beroperasi, dan bergerak ke luar ruang kerja.
Dialek yang tersedia untuk file BUILD
dan .bzl
sedikit berbeda
karena mereka mengekspresikan
hal yang berbeda. Daftar perbedaan tersedia
di sini.
Informasi selengkapnya tentang Starlark tersedia di sini.
Fase pemuatan/analisis
Fase pemuatan/analisis adalah di mana Bazel menentukan tindakan apa yang diperlukan untuk membuat aturan tertentu. Unit dasarnya adalah "target yang dikonfigurasi", yaitu, dengan cukup masuk akal, pasangan (target, konfigurasi).
Hal ini disebut "fase pemuatan/analisis" karena dapat dibagi menjadi dua bagian yang berbeda, yang dulunya diserialisasi, tetapi sekarang bisa tumpang tindih dalam waktu:
- Memuat paket, yaitu mengubah file
BUILD
menjadi objekPackage
yang mewakili mereka - Menganalisis target yang dikonfigurasi, yaitu menjalankan implementasi aturan untuk menghasilkan grafik tindakan
Setiap target yang dikonfigurasi dalam penutupan transitif target yang dikonfigurasi diminta pada baris perintah harus dianalisis dari bawah ke atas; yaitu, node daun pertama, lalu ke baris perintah. Input untuk analisis satu target yang dikonfigurasi adalah:
- Konfigurasi. ("bagaimana" untuk membuat aturan itu; misalnya, target tetapi juga hal-hal seperti opsi baris perintah yang diinginkan pengguna diteruskan ke compiler C++)
- Dependensi langsung. Penyedia info transitif mereka tersedia pada aturan yang dianalisis. Mereka disebut seperti itu karena menyediakan "gabungkan" informasi dalam penutupan transitif konfigurasi tertentu, seperti semua file .jar di classpath atau semua file .o yang harus ditautkan ke biner C++)
- Targetnya sendiri. Ini adalah hasil dari pemuatan paket yang ditargetkan berada di dalam. Untuk aturan, hal ini mencakup atributnya, yang biasanya merupakan itu penting.
- Implementasi target yang dikonfigurasi. Untuk aturan, parameter ini dapat dalam Starlark atau Java. Semua target yang dikonfigurasi tanpa aturan diterapkan pada Java.
Output dari menganalisis target yang dikonfigurasi adalah:
- Penyedia info transitif yang mengonfigurasi target yang bergantung padanya dapat akses
- Artefak yang dapat dibuat dan tindakan yang menghasilkannya.
API yang ditawarkan ke aturan Java adalah RuleContext
, yang setara dengan
Argumen ctx
aturan Starlark. API-nya lebih canggih, tetapi pada saat yang sama
lebih mudah, lebih mudah untuk melakukan Bad ThingsTM, misalnya untuk menulis kode yang waktu atau
kompleksitas ruang yang kuadrat (atau lebih buruk), untuk membuat server Bazel error dengan
Pengecualian Java atau untuk melanggar invarian (misalnya dengan secara tidak sengaja memodifikasi
Options
atau dengan membuat target yang dikonfigurasi dapat berubah)
Algoritma yang menentukan dependensi langsung dari target yang dikonfigurasi
tinggal di DependencyResolver.dependentNodeMap()
.
Konfigurasi
Konfigurasi adalah "bagaimana" dalam membangun target: untuk platform apa dan opsi baris perintah, dll.
Target yang sama dapat dibangun untuk beberapa konfigurasi dalam build yang sama. Ini berguna, misalnya, ketika kode yang sama digunakan untuk alat yang dijalankan selama build dan untuk kode target. Lalu kita melakukan kompilasi silang atau ketika kita membangun aplikasi Android gemuk (yang berisi kode native untuk beberapa CPU arsitektur)
Secara konseptual, konfigurasi adalah instance BuildOptions
. Namun, di
, BuildOptions
digabungkan oleh BuildConfiguration
yang memberikan
berbagai fungsi tambahan. Ia merambah dari atas
grafik dependensi ke bawah. Jika berubah, build harus
dianalisis ulang.
Hal ini menyebabkan anomali seperti harus menganalisis ulang seluruh build jika, jumlah pengujian yang diminta berubah, meskipun hanya memengaruhi target pengujian (kami berencana untuk "memangkas" konfigurasi agar hal ini bukan itu masalahnya, tapi belum siap).
Saat penerapan aturan memerlukan bagian dari konfigurasi, penerapan harus mendeklarasikan
nya dalam definisinya menggunakan RuleClass.Builder.requiresConfigurationFragments()
kami. Hal ini dilakukan untuk menghindari kesalahan (seperti aturan Python yang menggunakan fragmen Java) dan
untuk memfasilitasi pemangkasan konfigurasi sehingga
seperti jika opsi Python berubah,
target tidak perlu dianalisis ulang.
Konfigurasi aturan tidak harus sama dengan konfigurasi "induk" aturan. Proses mengubah konfigurasi dalam tepi dependensi disebut "transisi konfigurasi". Hal ini dapat terjadi di dua tempat:
- Di edge dependensi. Transisi ini ditetapkan dalam
Attribute.Builder.cfg()
dan merupakan fungsi dariRule
(dengan terjadi) danBuildOptions
(konfigurasi asli) ke satu atau lebihBuildOptions
(konfigurasi output). - Di setiap edge masuk ke target yang dikonfigurasi. Hal ini ditetapkan dalam
RuleClass.Builder.cfg()
.
Class yang relevan adalah TransitionFactory
dan ConfigurationTransition
.
Transisi konfigurasi digunakan, misalnya:
- Untuk mendeklarasikan bahwa dependensi tertentu digunakan selama proses build dan harus dibangun dalam arsitektur eksekusi
- Untuk mendeklarasikan bahwa dependensi tertentu harus dibangun untuk beberapa arsitektur (seperti untuk kode native di APK Android yang gemuk)
Jika transisi konfigurasi menghasilkan beberapa konfigurasi, ini disebut transisi terpisah.
Transisi konfigurasi juga dapat diimplementasikan di Starlark (dokumentasi di sini)
Penyedia info transitif
Penyedia info transitif adalah cara (dan _satu-satunya _way) untuk target yang dikonfigurasi untuk memberi tahu hal-hal tentang target lain yang dikonfigurasi yang bergantung padanya. Alasannya "transitif" dalam namanya adalah bahwa ini biasanya semacam penggabungan penutupan transitif dari target yang dikonfigurasi.
Umumnya ada korespondensi 1:1 antara penyedia info transitif Java
dan Starlark (pengecualiannya adalah DefaultInfo
yang merupakan penggabungan dari
FileProvider
, FilesToRunProvider
, dan RunfilesProvider
karena API tersebut
dianggap lebih Starlark daripada transliterasi langsung dari Java).
Kuncinya adalah salah satu hal berikut:
- Objek Kelas Java. Ini hanya tersedia untuk
penyedia yang tidak
dapat diakses dari Starlark. Penyedia ini adalah subclass dari
TransitiveInfoProvider
. - Sebuah {i>string<i}. Hal ini merupakan warisan dan sangat tidak
diinginkan karena rentan terhadap
bentrok nama. Penyedia info transitif tersebut adalah subclass langsung dari
build.lib.packages.Info
. - Simbol penyedia. Ini dapat dibuat dari Starlark menggunakan
provider()
dan merupakan cara yang direkomendasikan untuk membuat penyedia baru. Simbolnya adalah diwakili oleh instanceProvider.Key
di Java.
Penyedia baru yang diterapkan di Java harus diimplementasikan menggunakan BuiltinProvider
.
NativeProvider
tidak digunakan lagi (kita belum punya waktu untuk menghapusnya) dan
Subclass TransitiveInfoProvider
tidak dapat diakses dari Starlark.
Target yang dikonfigurasi
Target yang dikonfigurasi diterapkan sebagai RuleConfiguredTargetFactory
. Terdapat
untuk setiap class aturan yang diterapkan di Java. Target yang dikonfigurasi Starlark
dibuat melalui StarlarkRuleConfiguredTargetUtil.buildRule()
.
Factory target yang dikonfigurasi harus menggunakan RuleConfiguredTargetBuilder
untuk
mengkonstruksi nilai return. Kode ini terdiri dari hal-hal berikut:
filesToBuild
miliknya, konsep buram "kumpulan file aturan ini diwakili oleh variabel tersebut." Ini adalah file yang dibangun saat target yang dikonfigurasi berada di command line atau di src dari genrule.- {i>Runfile<i}, reguler, dan data.
- Grup output mereka. Ini adalah berbagai "kumpulan file lainnya" aturan tersebut bisa
buat. Mereka dapat diakses menggunakan atribut output_group dari
aturan filegroup di BUILD dan menggunakan penyedia
OutputGroupInfo
di Java.
{i>Runfile<i}
Beberapa biner memerlukan file data agar dapat dijalankan. Contoh yang jelas adalah pengujian yang memerlukan file input. Hal ini direpresentasikan dalam Bazel oleh konsep "runfiles". J "pohon runfiles" adalah pohon direktori dari file data untuk biner tertentu. Symlink dibuat di sistem file sebagai pohon symlink dengan masing-masing symlink yang menunjuk ke file dalam sumber hierarki output.
Kumpulan runfile direpresentasikan sebagai instance Runfiles
. Secara konseptual, ini adalah
memetakan dari jalur file dalam hierarki runfiles ke instance Artifact
yang
mewakilinya. Ini sedikit lebih rumit daripada satu Map
untuk dua
alasan:
- Sering kali, jalur {i>runfile<i} dari sebuah file sama dengan {i>execpath<i}-nya. Kita menggunakannya untuk menghemat RAM.
- Ada berbagai jenis entri lama dalam hierarki {i>runfile<i}, yang juga memerlukan untuk direpresentasikan.
Runfile dikumpulkan menggunakan RunfilesProvider
: instance dari class ini
merepresentasikan runfiles target yang dikonfigurasi (seperti library) dan resource transitifnya
kebutuhan {i>closure<i} dan mereka dikumpulkan seperti
satu set yang bersarang (pada kenyataannya, mereka
diimplementasikan menggunakan set bertingkat di bawah penutup): setiap target menggabungkan runfile
dependensinya, menambahkan beberapa dependensinya sendiri, lalu mengirimkan set yang dihasilkan
di grafik dependensi. Instance RunfilesProvider
berisi dua Runfiles
instance, satu ketika aturan bergantung pada melalui "data" dan
satu untuk setiap jenis dependensi
lain yang masuk. Hal ini karena target
terkadang menghadirkan runfile yang berbeda jika
bergantung pada atribut data
dibandingkan sebaliknya. Ini adalah perilaku lama yang tidak diinginkan dan belum kami lewatkan
belum dihapus.
Runfile biner direpresentasikan sebagai instance RunfilesSupport
. Ini
berbeda dengan Runfiles
karena RunfilesSupport
memiliki kemampuan
sedang dibangun (tidak seperti Runfiles
, yang hanya merupakan pemetaan). Ini
memerlukan komponen tambahan berikut:
- Manifes runfiles input. Ini adalah deskripsi berseri dari hierarki runfiles. Fungsi ini digunakan sebagai {i>proxy<i} untuk konten hierarki runfiles dan Bazel beranggapan bahwa pohon runfile berubah jika dan hanya jika isinya dari perubahan manifes.
- Manifes runfiles output. Ini digunakan oleh library runtime yang menangani pohon {i>runfile<i}, terutama di Windows, yang terkadang tidak mendukung {i>symbolic link<i}.
- Perantara runfile. Agar pohon {i>runfiles<i} ada, kita perlu untuk membangun pohon symlink dan artefak yang dituju symlink. Secara berurutan untuk mengurangi jumlah tepi dependensi, {i> middleman<i} runfiles dapat yang digunakan untuk mewakili semua ini.
- Argumen command line untuk menjalankan biner yang runfile-nya
yang diwakili oleh objek
RunfilesSupport
.
Aspek
Aspek adalah cara untuk "menyebarkan komputasi ke bawah grafik dependensi". Mereka adalah
dijelaskan untuk pengguna Bazel
di sini. Bagus
contoh yang memotivasi adalah buffering protokol: aturan proto_library
tidak boleh diketahui
tentang bahasa tertentu, tetapi membangun implementasi dari
pesan penyangga ("unit dasar" penyangga protokol) dalam pemrograman apa pun
bahasa harus digabungkan dengan aturan proto_library
,
sehingga jika dua target dalam
bahasa yang sama tergantung pada {i>buffer<i}
protokol yang sama, hanya akan dibangun sekali.
Sama seperti target yang dikonfigurasi, target tersebut direpresentasikan di Skyframe sebagai SkyValue
dan cara pembuatannya sangat mirip dengan cara target yang dikonfigurasi
dibangun: memiliki kelas factory
bernama ConfiguredAspectFactory
yang memiliki
akses ke RuleContext
, tetapi tidak seperti factory target yang dikonfigurasi, alat ini juga mengetahui
tentang target terkonfigurasi yang
terpasang dan penyedianya.
Serangkaian aspek yang disebarkan ke bawah grafik dependensi ditentukan untuk setiap
menggunakan fungsi Attribute.Builder.aspects()
. Ada beberapa
kelas dengan nama membingungkan yang berpartisipasi dalam proses:
AspectClass
adalah implementasi aspek. Bisa dalam bentuk Java (dalam hal ini itu adalah {i>subclass<i}) atau dalam Starlark (dalam hal ini itu adalah instanceStarlarkAspectClass
). Ini dapat dianalisa denganRuleConfiguredTargetFactory
.AspectDefinition
adalah definisi aspek; model ini mencakup penyedia yang dibutuhkan, penyedia yang disediakan, dan berisi referensi ke implementasinya, seperti instanceAspectClass
yang sesuai. Penting setara denganRuleClass
.AspectParameters
adalah cara untuk membuat parameter aspek yang disebarkan ke bawah grafik dependensi. Saat ini merupakan string ke peta string. Contoh yang bagus kegunaannya adalah buffering protokol: jika suatu bahasa memiliki beberapa API, informasi mengenai API mana yang harus dibuat buffering protokol harus akan disebarkan ke bawah grafik dependensi.Aspect
mewakili semua data yang diperlukan untuk menghitung aspek yang menyebar ke bawah grafik dependensi. Class ini terdiri dari class aspek, definisinya dan parameternya.RuleAspect
adalah fungsi yang menentukan aspek mana yang aturan tertentu harus diterapkan.Rule
-> fungsiAspect
.
Komplikasi yang agak tak terduga adalah bahwa
aspek dapat melekat pada aspek lain;
misalnya, aspek yang mengumpulkan classpath untuk Java IDE mungkin akan
ingin tahu tentang semua file .jar di classpath, tetapi beberapa di antaranya
{i>buffer<i} protokol. Dalam hal ini, aspek IDE perlu dilampirkan ke
(aturan proto_library
+ aspek proto Java).
Kompleksitas aspek pada aspek dibahas di class
AspectCollection
.
Platform dan toolchain
Bazel mendukung build multi-platform, yaitu build yang mungkin beberapa arsitektur di mana tindakan build berjalan dan beberapa arsitektur untuk kode mana yang dibangun. Arsitektur ini disebut sebagai platform dalam Bazel bahasa (dokumentasi lengkap di sini)
Platform dijelaskan oleh pemetaan nilai kunci dari setelan batasan (seperti
konsep "arsitektur CPU") ke nilai batasan (seperti CPU tertentu
seperti x86_64). Kami memiliki "kamus" batasan yang paling umum digunakan
setelan dan nilai dalam repositori @platforms
.
Konsep toolchain berasal dari fakta bahwa bergantung pada platform apa di mana build berjalan dan platform apa yang ditargetkan, orang mungkin perlu menggunakan kompilator yang berbeda; misalnya, toolchain C++ tertentu dapat berjalan pada OS tertentu dan dapat menargetkan beberapa OS lain. Bazel harus menentukan C++ compiler yang digunakan berdasarkan eksekusi yang ditetapkan dan platform target (dokumentasi untuk toolchain di sini).
Untuk melakukannya, toolchain dianotasi dengan set eksekusi dan menargetkan batasan platform yang mereka dukung. Untuk melakukannya, definisi dari toolchain dibagi menjadi dua bagian:
- Aturan
toolchain()
yang menjelaskan kumpulan eksekusi dan target yang didukung toolchain dan memberi tahu jenis (seperti C++ atau Java) toolchain tersebut (yang terakhir diwakili oleh aturantoolchain_type()
) - Aturan spesifik per bahasa yang menjelaskan toolchain aktual (seperti
cc_toolchain()
)
Hal ini dilakukan dengan cara ini karena kita perlu
mengetahui batasan untuk setiap
toolchain untuk melakukan resolusi toolchain dan spesifik per bahasa
Aturan *_toolchain()
berisi lebih banyak informasi dari itu, sehingga dibutuhkan lebih banyak
waktu pemuatan.
Platform eksekusi ditentukan dengan salah satu cara berikut:
- Di file WORKSPACE menggunakan fungsi
register_execution_platforms()
- Pada command line menggunakan baris perintah --extra_execution_platforms opsi
Set platform eksekusi yang tersedia dikomputasi dalam
RegisteredExecutionPlatformsFunction
.
Platform target untuk target yang dikonfigurasi ditentukan oleh
PlatformOptions.computeTargetPlatform()
. Ini adalah daftar platform karena kita
akhirnya ingin mendukung beberapa platform target, tetapi tidak diterapkan
.
Kumpulan toolchain yang akan digunakan untuk target yang dikonfigurasi ditentukan oleh
ToolchainResolutionFunction
. Ini adalah fungsi dari:
- Set toolchain terdaftar (dalam file WORKSPACE dan )
- Platform eksekusi dan target yang diinginkan (dalam konfigurasi)
- Sekumpulan jenis toolchain yang diperlukan oleh target yang dikonfigurasi (di
UnloadedToolchainContextKey)
- Set batasan platform eksekusi dari target yang dikonfigurasi (
atribut
exec_compatible_with
) dan konfigurasi (--experimental_add_exec_constraints_to_targets
), inciUnloadedToolchainContextKey
Hasilnya adalah UnloadedToolchainContext
, yang pada dasarnya adalah peta dari
jenis toolchain (direpresentasikan sebagai instance ToolchainTypeInfo
) ke label
toolchain yang dipilih. Ini disebut "dibongkar" karena tidak berisi
toolchain itu sendiri, hanya labelnya.
Kemudian, toolchain benar-benar dimuat menggunakan ResolvedToolchainContext.load()
dan digunakan oleh implementasi target
yang dikonfigurasi yang memintanya.
Kami juga memiliki sistem lama yang mengandalkan adanya satu "host"
dan konfigurasi target yang diwakili oleh berbagai
flag konfigurasi, seperti --cpu
. Kami secara bertahap akan melakukan transisi ke
sistem file. Untuk menangani kasus ketika pengguna mengandalkan konfigurasi lama
kami, kami telah menerapkan
pemetaan platform
untuk menerjemahkan antara tanda lama dan batasan platform gaya baru.
Kode mereka ada di PlatformMappingFunction
dan menggunakan "little" non-Starlark
"bahasa".
Batasan
Terkadang seseorang ingin menetapkan target sebagai hanya kompatibel dengan beberapa di berbagai platform Google. Sayangnya, Bazel memiliki beberapa mekanisme untuk mencapai tujuan ini:
- Batasan khusus aturan
environment_group()
/environment()
- Batasan platform
Batasan khusus aturan sebagian besar digunakan dalam Google untuk aturan Java; mereka
dalam perjalanan keluar dan itu tidak tersedia
di Bazel, tetapi kode sumbernya mungkin
berisi referensi
yang merujuk padanya. Atribut yang mengatur hal ini disebut
constraints=
.
environment_group() dan environment()
Aturan ini adalah mekanisme lama dan tidak digunakan secara luas.
Semua aturan build dapat mendeklarasikan "lingkungan" mana bisa dibangun, di mana
"lingkungan" adalah instance dari aturan environment()
.
Ada berbagai cara menentukan lingkungan yang didukung untuk aturan:
- Melalui atribut
restricted_to=
. Ini adalah bentuk yang paling langsung spesifikasi; perintah ini mendeklarasikan kumpulan lingkungan yang tepat yang didukung aturan untuk grup ini. - Melalui atribut
compatible_with=
. Perintah ini mendeklarasikan lingkungan sebagai aturan selain model "standar" yang didukung oleh secara default. - Melalui atribut tingkat paket
default_restricted_to=
dandefault_compatible_with=
. - Melalui spesifikasi default dalam aturan
environment_group()
. Setiap milik grup rekan yang secara tematik terkait (seperti "CPU arsitektur", "Versi JDK" atau "sistem operasi seluler"). Tujuan definisi grup lingkungan mencakup manakah dari harus didukung oleh "default" jika tidak ditentukan lain oleh Atributrestricted_to=
/environment()
. Aturan yang tidak memiliki akan mewarisi semua nilai default. - Melalui default class aturan. Setelan ini akan menggantikan setelan default global untuk semua
dari class aturan yang ditentukan. Ini dapat digunakan, misalnya, untuk membuat
semua aturan
*_test
dapat diuji tanpa setiap instance harus secara eksplisit mendeklarasikan kemampuan ini.
environment()
diterapkan sebagai aturan reguler sedangkan environment_group()
baik subclass dari Target
tetapi bukan Rule
(EnvironmentGroup
) dan
yang tersedia secara {i>default<i} dari Starlark
(StarlarkLibrary.environmentGroup()
) yang pada akhirnya menciptakan eponim
target. Hal ini untuk menghindari ketergantungan
siklik yang akan muncul karena setiap
lingkungan harus mendeklarasikan grup lingkungannya dan masing-masing
grup lingkungan harus mendeklarasikan
lingkungan defaultnya.
Sebuah build dapat dibatasi untuk lingkungan tertentu dengan
Opsi command line --target_environment
.
Implementasi pemeriksaan batasan sudah
RuleContextConstraintSemantics
dan TopLevelConstraintSemantics
.
Batasan platform
"resmi" saat ini untuk mendeskripsikan platform apa yang kompatibel dengan target yaitu dengan menggunakan batasan yang sama dengan yang digunakan untuk menjelaskan toolchain dan platform. Dalam peninjauan di permintaan pull #10945.
Visibilitas
Jika Anda mengerjakan codebase besar dengan banyak developer (seperti di Google), ingin mencegah orang lain untuk secara acak bergantung pada pada kode sumber. Jika tidak, sesuai dengan hukum Hyrum, orang akan bergantung pada perilaku yang Anda anggap dapat diterapkan spesifikasi pendukung.
Bazel mendukung ini dengan mekanisme yang disebut visibilitas: Anda dapat mendeklarasikan bahwa target tertentu hanya dapat diandalkan pada penggunaan visibilitas. Ini sedikit khusus karena, meskipun memiliki daftar label, label dapat mengenkode pola melalui nama paket daripada pointer ke target tertentu. (Ya, ini adalah cacat desain.)
Perubahan ini diterapkan di tempat berikut:
- Antarmuka
RuleVisibility
mewakili deklarasi visibilitas. Teknologi ini dapat berupa konstanta (sepenuhnya publik atau sepenuhnya pribadi) atau daftar label. - Label dapat merujuk ke salah satu grup paket (daftar paket yang ditentukan sebelumnya), untuk
paket secara langsung (
//pkg:__pkg__
) atau subpohon paket (//pkg:__subpackages__
). Ini berbeda dengan sintaks baris perintah, yang menggunakan//pkg:*
atau//pkg/...
. - Grup paket diterapkan sebagai targetnya sendiri (
PackageGroup
) dan target yang dikonfigurasi (PackageGroupConfiguredTarget
). Kita mungkin bisa gantilah dengan aturan sederhana jika kita mau. Logikanya diimplementasikan dengan bantuan:PackageSpecification
, yang sesuai dengan pola tunggal seperti//pkg/...
;PackageGroupContents
, yang sesuai ke atributpackages
package_group
tunggal; danPackageSpecificationProvider
, yang merupakan gabungan melaluipackage_group
danincludes
-nya yang transitif. - Konversi dari daftar label visibilitas ke dependensi dilakukan di
DependencyResolver.visitTargetVisibility
dan beberapa jenis lainnya tempat. - Pemeriksaan yang sebenarnya
dilakukan di
CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()
Kumpulan bertingkat
Sering kali, target yang dikonfigurasi menggabungkan serangkaian file dari dependensinya, menambahkan dirinya sendiri, dan menggabungkan kumpulan agregat ke penyedia info transitif sehingga target yang dikonfigurasi dan bergantung padanya dapat melakukan hal yang sama. Contoh:
- File header C++ yang digunakan untuk build
- File objek yang mewakili penutupan transitif
cc_library
- Kumpulan file .jar yang harus ada di classpath untuk aturan Java untuk mengompilasi atau menjalankan
- Kumpulan file Python dalam penutupan transitif aturan Python
Jika kita melakukan ini dengan cara naif menggunakan, misalnya, List
atau Set
, kita akan mendapatkan
penggunaan memori kuadrat: jika ada rantai aturan N dan setiap aturan menambahkan
kita memiliki 1+2+...+N anggota koleksi.
Untuk mengatasi masalah ini, kami
memiliki konsep sebuah
NestedSet
. Ini adalah struktur data yang terdiri dari NestedSet
lainnya
dan beberapa anggotanya sendiri, sehingga membentuk grafik asiklik terarah
dari {i>dataset<i}. ID tersebut tidak dapat diubah dan anggotanya dapat diiterasi. Kami mendefinisikan
beberapa urutan iterasi (NestedSet.Order
): praorder, postorder, topologi
({i>node<i} selalu muncul setelah ancestor-nya) dan "tidak peduli, tetapi seharusnya
sama setiap kali".
Struktur data yang sama disebut depset
di Starlark.
Artefak dan Tindakan
Versi sebenarnya terdiri dari serangkaian perintah yang perlu dijalankan untuk menghasilkan
{i>output<i} yang diinginkan pengguna. Perintah-perintah tersebut dinyatakan sebagai instance
class Action
dan file direpresentasikan sebagai instance class
Artifact
. Mereka tersusun dalam grafik bipartit, terarah, asiklik yang disebut
"grafik tindakan".
Artefak terdiri dari dua jenis: artefak sumber (artefak yang tersedia sebelum Bazel mulai mengeksekusi) dan artefak turunan (yang perlu dibuat). Artefak turunan bisa berupa beberapa jenis:
- **Artefak reguler. **Data ini diperiksa untuk mengetahui informasi terbaru dengan menghitung {i>checksum<i} mereka, dengan {i>mtime<i} sebagai pintasan; kita tidak melakukan {i>checksum<i} pada file jika {i>ctime<i} belum berubah.
- Artefak symlink yang belum di-resolve. Dokumen ini diperiksa untuk mengetahui memanggil readlink(). Tidak seperti artefak biasa, ini bisa menggantung {i>symlink<i}. Biasanya digunakan dalam kasus di mana satu file kemudian mengemas beberapa file ke dalam semacam arsip.
- Artefak pohon. Ini bukan file tunggal, melainkan pohon direktori. Mereka
diperiksa {i>up-to-date<i} dengan cara
memeriksa kumpulan file di dalamnya dan
konten. Class tersebut direpresentasikan sebagai
TreeArtifact
. - Artefak metadata konstan. Perubahan pada artefak ini tidak memicu membangun kembali. Ini digunakan secara eksklusif untuk informasi cap build: kita tidak ingin melakukan {i>rebuild<i} hanya karena waktu saat ini berubah.
Tidak ada alasan mendasar mengapa artefak sumber
tidak boleh berupa artefak pohon atau
artefak symlink yang belum terselesaikan, hanya saja
kita belum mengimplementasikannya (kita
seharusnya -- merujuk ke direktori sumber dalam file BUILD
adalah salah satu
beberapa masalah kesalahan yang sudah lama ada pada Bazel; kita punya
pekerjaan semacam itu, yang dimungkinkan oleh
properti JVM BAZEL_TRACK_SOURCE_DIRECTORIES=1
)
Jenis Artifact
yang terkenal adalah perantara. Hal ini ditunjukkan dengan Artifact
instance yang merupakan output dari MiddlemanAction
. {i>Mockup <i}digunakan untuk
beberapa hal khusus:
- Agregat perantara digunakan untuk mengelompokkan artefak bersama-sama. Ini agar jika banyak tindakan menggunakan set input besar yang sama, kita tidak memiliki N*M tepi dependensi, hanya N+M (mereka diganti dengan kumpulan bertingkat)
- Penjadwalan perantara dependensi memastikan bahwa tindakan berjalan sebelum tindakan lainnya.
Skrip ini sebagian besar digunakan untuk analisis lint tetapi juga untuk kompilasi C++ (lihat
CcCompilationContext.createMiddleman()
untuk penjelasan) - Perantara Runfile digunakan untuk memastikan adanya hierarki {i>runfile<i} sehingga yang tidak secara terpisah perlu bergantung pada manifes {i>output<i} dan setiap satu artefak yang dirujuk oleh hierarki runfiles.
Tindakan paling baik dipahami sebagai perintah yang perlu dijalankan, yaitu lingkungan yang dibutuhkannya dan serangkaian {i> output<i} yang dihasilkannya. Hal-hal berikut adalah hal utama komponen deskripsi suatu tindakan:
- Command line yang perlu dijalankan
- Artefak input yang dibutuhkan
- Variabel lingkungan yang perlu ditetapkan
- Anotasi yang menggambarkan lingkungan (misalnya platform) yang perlu dijalankan \
Ada juga beberapa kasus khusus lainnya, seperti
menulis file yang isinya
yang dikenal Bazel. Class tersebut adalah subclass AbstractAction
. Sebagian besar tindakan
SpawnAction
atau StarlarkAction
(sama saja, keduanya boleh dibilang tidak
terpisah), meskipun Java dan C++ memiliki jenis tindakannya sendiri
(JavaCompileAction
, CppCompileAction
, dan CppLinkAction
).
Kami akhirnya ingin memindahkan semuanya ke SpawnAction
; JavaCompileAction
sama dengan
cukup mendekati, tetapi C++ merupakan kasus
khusus karena penguraian file {i> .d<i} dan
termasuk pemindaian.
Grafik tindakan sebagian besar "disematkan" ke dalam grafik Skyframe: secara konseptual,
eksekusi tindakan direpresentasikan
sebagai pemanggilan
ActionExecutionFunction
. Pemetaan dari edge dependensi grafik tindakan ke
Edge dependensi Skyframe dijelaskan di
ActionExecutionFunction.getInputDeps()
dan Artifact.key()
, serta memiliki beberapa
pengoptimalan untuk mempertahankan jumlah tepi Skyframe tetap rendah:
- Artefak turunan tidak memiliki
SkyValue
-nya sendiri. Sebagai gantinya,Artifact.getGeneratingActionKey()
digunakan untuk mengetahui kunci untuk tindakan yang menghasilkan - Kumpulan bertingkat memiliki kunci Skyframe sendiri.
Tindakan yang dibagikan
Beberapa tindakan dihasilkan oleh beberapa target yang dikonfigurasi; Aturan Starlark adalah lebih terbatas karena mereka hanya diizinkan untuk menempatkan tindakan turunan ke dalam direktori yang ditentukan oleh konfigurasi dan paketnya (meskipun demikian, aturan dalam paket yang sama bisa bertentangan), tetapi aturan yang diterapkan di Java dapat turunan dari mana saja.
Hal ini dianggap sebagai suatu kesalahan fitur, tetapi menyingkirkannya sangat sulit karena menghasilkan penghematan waktu eksekusi yang signifikan saat, misalnya, file sumber perlu diproses entah bagaimana dan file itu dirujuk oleh beberapa aturan (gelombang tangan). Hal ini membutuhkan beberapa RAM: masing-masing RAM {i>instance<i} dari tindakan bersama perlu disimpan dalam memori secara terpisah.
Jika dua tindakan menghasilkan file output yang sama, keduanya harus sama persis:
memiliki input, {i>output<i} yang sama, dan
menjalankan baris perintah yang sama. Ini
kesetaraan diimplementasikan di Actions.canBeShared()
dan
diverifikasi antara fase analisis dan eksekusi dengan melihat setiap Tindakan.
Hal ini diterapkan di SkyframeActionExecutor.findAndStoreArtifactConflicts()
dan merupakan salah satu dari beberapa tempat di Bazel yang memerlukan tampilan
buat.
Fase eksekusi
Pada saat inilah Bazel benar-benar mulai menjalankan tindakan {i>build<i}, seperti perintah yang menghasilkan {i>output<i}.
Hal pertama yang dilakukan Bazel setelah
fase analisis adalah menentukan apa
Artefak perlu dibangun. Logika untuk ini dikodekan dalam
TopLevelArtifactHelper
; secara garis besar, itu adalah filesToBuild
dari
target yang dikonfigurasi pada baris
perintah dan konten {i>output<i} khusus
dengan tujuan eksplisit untuk menyatakan
"jika target ini berada di dalam perintah
ini, bangun artefak ini".
Langkah selanjutnya adalah membuat root eksekusi. Karena Bazel memiliki
opsi untuk membaca
paket sumber dari lokasi yang berbeda dalam sistem file (--package_path
),
aplikasi harus menyediakan tindakan yang dieksekusi secara lokal dengan hierarki sumber lengkap. Ini adalah
ditangani oleh class SymlinkForest
dan bekerja dengan mencatat setiap target
digunakan dalam fase analisis dan membangun
hierarki direktori tunggal yang menghubungkan
setiap paket dengan target yang
digunakan dari lokasi sebenarnya. Alternatifnya akan
adalah meneruskan jalur yang benar ke perintah (dengan mempertimbangkan --package_path
).
Hal ini tidak diinginkan karena:
- Mengubah command line tindakan saat paket dipindahkan dari jalur paket entri ke akun lain (sebelumnya umum terjadi)
- Ini menghasilkan baris perintah yang berbeda jika tindakan dijalankan dari jarak jauh berjalan secara lokal,
- Diperlukan transformasi command line khusus untuk alat yang digunakan (pertimbangkan perbedaan antara seperti classpath Java dan jalur penyertaan C++)
- Mengubah command line suatu tindakan akan membatalkan entri cache tindakannya
--package_path
secara bertahap dan terus-menerus tidak digunakan lagi
Kemudian, Bazel mulai melintasi grafik aksi (grafik terarah bipartit
yang terdiri dari tindakan dan artefak input dan outputnya) dan menjalankan tindakan.
Eksekusi setiap tindakan direpresentasikan oleh instance SkyValue
kelas ActionExecutionValue
.
Karena menjalankan tindakan itu mahal, kita memiliki beberapa lapisan {i>caching<i} yang dapat terkena serangan di belakang Skyframe:
ActionExecutionFunction.stateMap
berisi data untuk membuat Skyframe dimulai ulang dariActionExecutionFunction
murah- Cache tindakan lokal berisi data tentang status sistem file
- Sistem eksekusi jarak jauh biasanya juga berisi cache-nya sendiri
Cache tindakan lokal
{i>Cache<i} ini adalah lapisan lain yang berada di belakang Skyframe; bahkan jika suatu tindakan dijalankan kembali di Skyframe, masih bisa menjadi hit di cache tindakan lokal. Ini mewakili keadaan sistem file lokal dan diserialisasi ke {i>disk<i} yang berarti bahwa ketika seseorang menjalankan server Bazel baru, seseorang bisa mendapatkan {i>cache<i} tindakan lokal jika grafik Skyframe kosong.
Cache ini diperiksa untuk menemukan hit menggunakan metode
ActionCacheChecker.getTokenIfNeedToExecute()
.
Berbeda dengan namanya, ini adalah peta dari jalur artefak turunan ke tindakan yang memunculkannya. Tindakan tersebut dideskripsikan sebagai:
- Kumpulan file {i>input<i} dan {i>output<i} dan {i>checksum<i}-nya
- "Kunci aksinya", yang biasanya
merupakan baris perintah yang dijalankan, tetapi
secara umum, mewakili segala sesuatu yang
tidak diambil oleh {i>checksum <i}dari
file input (seperti untuk
FileWriteAction
, ini adalah checksum data yang tertulis)
Ada juga “{i>cache<i} tindakan {i>top-down<i} yang sangat eksperimental yang masih dalam pengembangan, yang menggunakan {i>hash <i} transitif untuk menghindari masuk ke {i>cache<i} sebanyak kali.
Penemuan input dan pemangkasan input
Beberapa tindakan lebih rumit daripada sekadar memiliki satu set input. Perubahan pada satu set input dari suatu tindakan datang dalam dua bentuk:
- Suatu tindakan dapat menemukan {i>input<i} baru sebelum dieksekusi atau memutuskan bahwa beberapa
inputnya sebenarnya tidak diperlukan. Contoh kanonis adalah C++,
di mana lebih baik membuat perkiraan
yang matang tentang file {i>header<i} apa yang
digunakan oleh file dari penutupan transitifnya
sehingga kita tidak memperhatikan setiap pengiriman
ke eksekutor jarak jauh; Oleh karena itu, kita memiliki pilihan untuk
tidak mendaftarkan setiap
sebagai "input", tetapi memindai file sumber secara transitif
header yang disertakan dan hanya menandai file header tersebut sebagai input yang
yang disebutkan dalam pernyataan
#include
(kami memberikan perkiraan yang lebih tinggi sehingga kami tidak perlu mengimplementasikan praprosesor C penuh) Opsi ini saat ini dihubungkan dengan kabel untuk "salah" di Bazel dan hanya digunakan di Google. - Suatu tindakan mungkin menyadari bahwa beberapa file tidak digunakan selama eksekusinya. Di beberapa C++, ini disebut "file .d": compiler memberi tahu file {i>header<i} mana yang digunakan sesuai fakta, dan untuk menghindari rasa malu karena inkrementalitas daripada Make, Bazel memanfaatkan fakta ini. Hal ini menawarkan dibandingkan dengan pemindai {i>include<i} karena mengandalkan kompiler.
Hal ini diimplementasikan menggunakan metode pada Tindakan:
Action.discoverInputs()
dipanggil. Ini akan mengembalikan serangkaian perangkat yang bersarang Artefak yang dianggap wajib. Ini harus berupa artefak sumber sehingga tidak ada tepi dependensi di grafik tindakan yang tidak memiliki dalam grafik target yang dikonfigurasi.- Tindakan ini dijalankan dengan memanggil
Action.execute()
. - Di akhir
Action.execute()
, tindakan dapat memanggilAction.updateInputs()
untuk memberi tahu Bazel bahwa tidak semua inputnya diperlukan. Hal ini dapat mengakibatkan build inkremental yang salah jika input yang digunakan dilaporkan sebagai tidak terpakai.
Saat cache tindakan menampilkan hit pada instance Action baru (seperti dibuat
setelah server dimulai ulang), Bazel memanggil updateInputs()
sendiri sehingga kumpulan
mencerminkan hasil penemuan dan pemangkasan input yang dilakukan sebelumnya.
Tindakan Starlark dapat memanfaatkan fasilitas ini untuk mendeklarasikan beberapa input sebagai tidak digunakan
menggunakan argumen unused_inputs_list=
dari
ctx.actions.run()
.
Berbagai cara untuk menjalankan tindakan: Strategi/ActionContexts
Beberapa tindakan dapat dijalankan dengan berbagai cara. Misalnya, baris perintah dapat
dijalankan secara lokal, tetapi dalam berbagai jenis sandbox, atau dari jarak jauh. Tujuan
konsep yang mewujudkannya disebut ActionContext
(atau Strategy
, karena kita
berhasil berjalan hanya setengah jalan dengan penggantian nama...)
Siklus hidup konteks tindakan adalah sebagai berikut:
- Saat fase eksekusi dimulai, instance
BlazeModule
ditanyai apa konteks tindakan yang mereka miliki. Ini terjadi di konstruktorExecutionTool
. Jenis konteks tindakan diidentifikasi olehClass
Java yang merujuk pada sub-antarmuka dariActionContext
dan yang harus diimplementasikan oleh konteks tindakan. - Konteks tindakan yang tepat dipilih dari yang tersedia dan
diteruskan ke
ActionExecutionContext
danBlazeExecutor
. - Tindakan meminta konteks menggunakan
ActionExecutionContext.getContext()
danBlazeExecutor.getStrategy()
(seharusnya hanya ada satu cara untuk melakukan itu...)
Strategi bebas menyebutkan strategi lain dalam melakukan pekerjaan mereka; ini digunakan, untuk misalnya, dalam strategi dinamis yang memulai tindakan baik secara lokal maupun jarak jauh, kemudian menggunakan opsi yang selesai lebih dahulu.
Salah satu strategi terkenal adalah strategi yang mengimplementasikan proses pekerja yang persisten
(WorkerSpawnStrategy
). Idenya adalah, beberapa alat memiliki waktu {i>startup<i} yang lama
dan karenanya harus digunakan kembali antartindakan alih-alih memulai yang baru untuk
setiap tindakan (Ini merupakan potensi masalah yang benar, karena Bazel
mengandalkan janji proses pekerja yang tidak membawa data yang dapat diamati
status di antara permintaan individual)
Jika alat berubah, proses pekerja perlu dimulai ulang. Apakah seorang pekerja
dapat digunakan kembali ditentukan dengan menghitung {i>checksum<i} untuk alat yang digunakan
WorkerFilesHash
. Hal ini bergantung pada mengetahui input mana dari tindakan tersebut yang mewakili
sebagai bagian dari {i>tool<i} dan
yang mewakili {i>input<i}; ini ditentukan oleh kreator
Action: Spawn.getToolFiles()
dan runfile Spawn
dihitung sebagai bagian dari alat.
Informasi selengkapnya tentang strategi (atau konteks tindakan!):
- Tersedia informasi tentang berbagai strategi untuk menjalankan tindakan di sini.
- Informasi tentang strategi dinamis, tempat kami menjalankan tindakan secara lokal dan jarak jauh untuk melihat penyelesaian mana yang lebih dulu tersedia. di sini.
- Informasi tentang seluk-beluk menjalankan tindakan secara lokal tersedia di sini.
Pengelola sumber daya lokal
Bazel dapat menjalankan banyak tindakan secara paralel. Jumlah tindakan lokal yang harus dijalankan secara paralel akan berbeda dari satu tindakan ke tindakan lainnya: makin banyak resource tindakan diperlukan, semakin sedikit {i>instance <i} yang harus berjalan pada saat yang sama untuk menghindari membebani komputer lokal.
Hal ini diterapkan di class ResourceManager
: setiap tindakan harus
dianotasi dengan perkiraan sumber daya lokal yang diperlukan dalam bentuk
Instance ResourceSet
(CPU dan RAM). Kemudian, ketika konteks tindakan melakukan sesuatu
yang memerlukan resource lokal, mereka memanggil ResourceManager.acquireResources()
dan diblokir sampai sumber daya yang
diperlukan tersedia.
Deskripsi lebih detail tentang pengelolaan sumber daya lokal tersedia di sini.
Struktur direktori output
Setiap tindakan memerlukan tempat terpisah di direktori output tempat tindakan tersebut ditempatkan output-nya. Lokasi artefak turunan biasanya sebagai berikut:
$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>
Bagaimana nama direktori yang terkait dengan menentukan konfigurasi? Ada dua properti yang diinginkan bertentangan:
- Jika dua konfigurasi bisa muncul di build yang sama, keduanya harus memiliki direktori yang berbeda sehingga keduanya dapat memiliki versi mereka sendiri yang sama tindakan; jika tidak sepakat mengenai perbedaan pendapat, dari aksi yang menghasilkan file {i>output<i} yang sama, Bazel tidak tahu mana tindakan yang harus dipilih ("konflik tindakan")
- Jika dua konfigurasi mewakili "kira-kira" hal yang sama, mereka harus memiliki nama yang sama sehingga tindakan yang dieksekusi di satu dapat digunakan kembali jika baris perintah cocok: misalnya, perubahan opsi baris perintah untuk compiler Java seharusnya tidak mengakibatkan tindakan kompilasi C++ dijalankan ulang.
Sejauh ini, kami belum menemukan cara pasti untuk memecahkan masalah ini, yang memiliki kesamaan dengan masalah {i>trimming<i} konfigurasi. Diskusi yang lebih panjang opsi tersedia di sini. Area bermasalah utama adalah aturan Starlark (yang penulisnya biasanya sangat akrab dengan Bazel) dan aspek-aspek, yang menambahkan dimensi lain pada ruang lingkup yang dapat menghasilkan hal yang sama file output.
Pendekatan saat ini adalah bahwa
segmen jalur untuk konfigurasi itu
<CPU>-<compilation mode>
dengan berbagai akhiran ditambahkan sehingga konfigurasi
transisi yang diimplementasikan dalam Java tidak mengakibatkan konflik tindakan. Selain itu,
{i>checksum<i} dari rangkaian transisi konfigurasi Starlark
ditambahkan sehingga pengguna
tidak dapat menyebabkan konflik tindakan. Ini jauh dari sempurna. Hal ini diimplementasikan di
OutputDirectories.buildMnemonic()
dan bergantung pada setiap fragmen konfigurasi
menambahkan bagiannya sendiri
ke nama direktori {i>output<i}.
Pengujian
Bazel memiliki dukungan yang kaya untuk menjalankan pengujian. API ini mendukung:
- Menjalankan pengujian dari jarak jauh (jika backend eksekusi jarak jauh tersedia)
- Menjalankan pengujian beberapa kali secara paralel (untuk deflaking atau pengumpulan waktu tertentu)
- Pengujian sharding (membagi kasus pengujian dalam pengujian yang sama melalui beberapa proses untuk kecepatan)
- Menjalankan kembali pengujian yang tidak stabil
- Mengelompokkan pengujian ke dalam rangkaian pengujian
Pengujian adalah target yang dikonfigurasi secara reguler yang memiliki TestProvider, yang menjelaskan bagaimana pengujian harus dijalankan:
- Artefak yang menghasilkan build dalam pengujian sedang dijalankan. Ini adalah “{i>cache<i}
status" file yang berisi pesan
TestResultData
serial - Berapa kali pengujian harus dijalankan
- Jumlah shard yang harus dibagi menjadi pengujian
- Beberapa parameter tentang cara pengujian harus dijalankan (seperti waktu tunggu pengujian)
Menentukan pengujian yang akan dijalankan
Menentukan pengujian yang dijalankan adalah proses yang rumit.
Pertama, selama penguraian pola target, rangkaian pengujian diperluas secara rekursif. Tujuan
ekspansi diterapkan di TestsForTargetPatternFunction
. Agak peduli
yang mengejutkan adalah jika {i>
test suite<i} mendeklarasikan tidak ada pengujian, itu mengacu pada
setiap pengujian dalam paketnya. Hal ini diterapkan di Package.beforeBuild()
dengan
menambahkan atribut implisit yang disebut $implicit_tests
ke aturan rangkaian pengujian.
Kemudian, pengujian difilter untuk ukuran, tag, waktu tunggu, dan bahasa sesuai dengan
opsi command line. Hal ini diimplementasikan di TestFilter
dan dipanggil dari
TargetPatternPhaseFunction.determineTests()
selama penguraian target dan
hasil dimasukkan ke dalam TargetPatternPhaseValue.getTestsToRunLabels()
. Alasannya
mengapa atribut aturan yang dapat difilter
tidak dapat dikonfigurasi adalah bahwa
terjadi sebelum fase analisis, oleh karena itu, konfigurasi tidak
yang tersedia.
Tindakan ini kemudian diproses lebih lanjut di BuildView.createResult()
: target yang
analisis yang gagal disaring dan pengujian
dibagi menjadi analisis eksklusif dan
pengujian non-eksklusif. Hasilnya kemudian dimasukkan ke dalam AnalysisResult
, yang merupakan cara
ExecutionTool
mengetahui pengujian yang akan dijalankan.
Untuk memberikan transparansi pada proses yang rumit ini, tests()
operator kueri (diterapkan di TestsFunction
) tersedia untuk mengetahui pengujian mana
dijalankan ketika target tertentu
ditetapkan pada baris perintah. Penting
sayangnya implementasi ulang, jadi
mungkin menyimpang dari yang disebutkan di atas
dengan berbagai cara.
Menjalankan pengujian
Cara pengujian dijalankan adalah dengan meminta artefak status cache. Hal ini kemudian
menghasilkan eksekusi TestRunnerAction
, yang pada akhirnya memanggil metode
TestActionContext
yang dipilih oleh opsi command line --test_strategy
menjalankan pengujian dengan cara yang diminta.
Pengujian dijalankan sesuai dengan protokol rumit yang menggunakan variabel lingkungan untuk memberi tahu pengujian apa yang diharapkan dari mereka. Deskripsi tentang Bazel yang diharapkan dari pengujian dan pengujian yang dapat diharapkan dari Bazel di sini. Di paling sederhana, kode keluar 0 berarti berhasil, yang lain berarti kegagalan.
Selain file status cache, setiap proses pengujian memberikan sejumlah
. Log tersebut ditempatkan di "direktori log pengujian" yang merupakan subdirektori bernama
testlogs
dari direktori output konfigurasi target:
test.xml
, file XML gaya JUnit yang menjelaskan setiap kasus pengujian di shard pengujiantest.log
, output konsol pengujian. {i>stdout<i} dan {i>stderr<i} tidak secara terpisah.test.outputs
, "direktori output yang tidak dideklarasikan"; ini digunakan dalam pengujian yang ingin menghasilkan {i>output<i} file selain apa yang mereka cetak ke terminal.
Ada dua hal yang bisa terjadi selama pelaksanaan uji coba membangun target reguler: eksekusi uji eksklusif dan streaming output.
Beberapa pengujian perlu dijalankan dalam mode eksklusif, misalnya tidak secara paralel dengan
pengujian lainnya. Hal ini dapat diperoleh dengan menambahkan tags=["exclusive"]
ke
aturan pengujian atau menjalankan pengujian dengan --test_strategy=exclusive
. Setiap penawaran eksklusif
dijalankan dengan pemanggilan Skyframe terpisah yang meminta eksekusi
uji setelah "utama" buat. Hal ini diimplementasikan di
SkyframeExecutor.runExclusiveTest()
.
Tidak seperti tindakan reguler, yang output terminalnya dibuang saat tindakan
selesai, pengguna dapat meminta output pengujian untuk di-streaming sehingga
mendapatkan informasi tentang kemajuan pengujian yang berjalan lama. Hal ini ditentukan oleh
Opsi command line --test_output=streamed
dan menyiratkan pengujian eksklusif
sehingga {i>output<i} dari pengujian yang berbeda tidak diselingi.
Hal ini diimplementasikan di class StreamedTestOutput
yang diberi nama dengan tepat dan digunakan dengan
polling perubahan pada file test.log
dari pengujian yang dimaksud dan membuang
{i>byte<i} ke terminal
tempat Bazel beraturan.
Hasil pengujian yang dilakukan tersedia di bus peristiwa dengan mengamati
berbagai peristiwa (seperti TestAttempt
, TestResult
, atau TestingCompleteEvent
).
Peristiwa tersebut dibuang ke Build Event Protocol dan dikirim ke konsol
paling lambat AggregatingTestListener
.
Pengumpulan cakupan
Cakupan dilaporkan oleh pengujian dalam format LCOV dalam file
bazel-testlogs/$PACKAGE/$TARGET/coverage.dat
.
Untuk mengumpulkan cakupan, setiap eksekusi uji digabungkan dalam skrip yang disebut
collect_coverage.sh
.
Skrip ini menyiapkan lingkungan pengujian untuk mengaktifkan pengumpulan cakupan dan menentukan di mana file cakupan ditulis oleh runtime cakupan. Kemudian sistem menjalankan pengujian. Sebuah pengujian dapat menjalankan beberapa subproses dan terdiri dari bagian yang ditulis dalam berbagai bahasa pemrograman yang berbeda (dengan bagian runtime kumpulan cakupan). Skrip wrapper bertanggung jawab untuk mengonversi file yang dihasilkan ke format LCOV jika perlu, dan menggabungkannya menjadi satu .
Interposisi collect_coverage.sh
dilakukan oleh strategi pengujian dan
mengharuskan collect_coverage.sh
untuk berada di input pengujian. Ini adalah
dicapai dengan atribut implisit :coverage_support
yang diselesaikan menjadi
nilai flag konfigurasi --coverage_support
(lihat
TestConfiguration.TestOptions.coverageSupport
)
Beberapa bahasa melakukan instrumentasi offline, artinya cakupan instrumentasi ditambahkan pada waktu kompilasi (seperti C++) dan yang lainnya melakukannya secara online instrumentasi, yang berarti instrumentasi cakupan ditambahkan saat eksekusi baik.
Konsep inti lainnya adalah cakupan dasar pengukuran. Ini adalah cakupan dari sebuah perpustakaan,
biner, atau menguji jika
tidak ada kode di dalamnya yang dijalankan. Masalah yang dipecahkan
adalah jika Anda
ingin menghitung cakupan pengujian untuk biner, tidak cukup untuk menggabungkan
cakupan semua tes karena mungkin
ada kode dalam biner yang tidak
ditautkan ke pengujian apa pun. Oleh karena itu, yang kita lakukan adalah
memancarkan file cakupan untuk setiap
biner yang hanya berisi file yang cakupannya kami kumpulkan tanpa ditanggung
penting. File cakupan dasar pengukuran untuk target berada pada
bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat
. ID ini juga dibuat
untuk biner dan pustaka selain untuk pengujian jika Anda meneruskan
--nobuild_tests_only
untuk Bazel.
Cakupan dasar saat ini rusak.
Kami melacak dua grup file untuk koleksi cakupan bagi setiap aturan: kumpulan file berinstrumen dan kumpulan file metadata instrumentasi.
Hanya itu kumpulan file berinstrumen, yaitu kumpulan file untuk diinstrumentasikan. Sebagai runtime cakupan online, ini dapat digunakan pada waktu {i>runtime<i} untuk memutuskan file mana instrumentasi. Cakupan juga digunakan untuk menerapkan cakupan dasar.
Kumpulan file metadata instrumentasi adalah kumpulan file tambahan yang diperlukan pengujian untuk menghasilkan file LCOV yang dibutuhkan Bazel darinya. Dalam praktiknya, ini terdiri dari file khusus runtime; misalnya, gcc memunculkan file .gcno selama kompilasi. Ini ditambahkan ke kumpulan input tindakan pengujian jika mode cakupan mengaktifkan pembuatan versi.
Apakah cakupan sedang dikumpulkan atau tidak disimpan dalam
BuildConfiguration
. Ini berguna karena merupakan cara mudah untuk mengubah pengujian
aksi dan grafik aksi tergantung pada bit ini, tetapi itu juga berarti bahwa jika
sedikit dibalik, semua target harus dianalisis ulang (beberapa bahasa, seperti
C++ memerlukan opsi compiler yang berbeda untuk
memancarkan kode yang dapat mengumpulkan,
yang agak mengurangi masalah ini, karena
analisis ulang tetap diperlukan).
File dukungan cakupan bergantung pada melalui label secara implisit dependensi sehingga dapat diganti oleh kebijakan pemanggilan, yang memungkinkan berbeda di antara versi Bazel yang berbeda. Idealnya, semua perbedaan akan dihilangkan, dan kami standarisasi pada salah satunya.
Kami juga membuat "laporan cakupan" yang menggabungkan cakupan yang dikumpulkan untuk
setiap pengujian dalam pemanggilan Bazel. Ini ditangani oleh
CoverageReportActionFactory
dan dipanggil dari BuildView.createResult()
. Ini
mendapatkan akses ke alat yang dibutuhkan dengan melihat :coverage_report_generator
dari pengujian pertama yang dijalankan.
Mesin kueri
Bazel memiliki bahasa kasar digunakan untuk menanyakan berbagai hal tentang berbagai grafik. Jenis kueri berikut yang tersedia:
bazel query
digunakan untuk menyelidiki grafik targetbazel cquery
digunakan untuk menyelidiki grafik target yang dikonfigurasibazel aquery
digunakan untuk menyelidiki grafik tindakan
Masing-masing diimplementasikan dengan membuat subclass AbstractBlazeQueryEnvironment
.
Fungsi kueri tambahan tambahan dapat dilakukan dengan membuat subclass QueryFunction
kami. Untuk mengizinkan hasil kueri streaming, alih-alih mengumpulkannya ke beberapa
struktur data, query2.engine.Callback
diteruskan ke QueryFunction
, yang
memanggilnya untuk hasil yang
ingin dikembalikan.
Hasil kueri dapat dimunculkan dengan berbagai cara: label, label, dan aturan
class, XML, protobuf dan sebagainya. Ini diimplementasikan sebagai subclass
OutputFormatter
.
Persyaratan halus dari beberapa format {i>output <i} kueri (tentunya proto) adalah Bazel perlu memunculkan _semua _informasi yang disediakan oleh pemuatan paket agar kita dapat melakukan diff pada {i>output<i} dan menentukan apakah target tertentu telah berubah. Akibatnya, nilai atribut harus dapat diserialisasi, itulah sebabnya hanya sedikit jenis atribut tanpa atribut yang memiliki Starlark yang kompleks masing-masing. Solusi yang biasa adalah menggunakan label, dan melampirkan kompleksitas informasi ke aturan dengan label tersebut. Ini bukan solusi yang sangat memuaskan dan sebaiknya Anda mencabut persyaratan ini.
Sistem modul
Bazel dapat diperluas dengan menambahkan modul ke dalamnya. Setiap modul harus dijadikan subclass
BlazeModule
(nama ini adalah relik sejarah Bazel yang dulunya
yang disebut Blaze) dan mendapatkan informasi tentang
berbagai peristiwa selama eksekusi
perintah.
Mereka sebagian besar digunakan untuk mengimplementasikan berbagai bagian fungsionalitas bahwa hanya beberapa versi Bazel (seperti yang kami gunakan di Google) yang memerlukan:
- Antarmuka ke sistem eksekusi jarak jauh
- Perintah baru
Rangkaian titik ekstensi yang ditawarkan BlazeModule
agak berbahaya. Larangan
menggunakannya sebagai contoh prinsip desain yang baik.
{i>Bus<i} acara
Cara utama BlazeModules berkomunikasi dengan anggota Bazel lainnya adalah dengan bus acara
(EventBus
): instance baru dibuat untuk setiap build, berbagai bagian Bazel
bisa memposting peristiwa ke sana dan modul bisa mendaftarkan pemroses untuk peristiwa
membuat Anda tertarik. Misalnya, hal-hal berikut ditampilkan sebagai peristiwa:
- Daftar target build yang akan dibangun telah ditentukan
(
TargetParsingCompleteEvent
) - Konfigurasi tingkat teratas telah ditentukan
(
BuildConfigurationEvent
) - Target dibuat, berhasil atau tidak (
TargetCompleteEvent
) - Pengujian dijalankan (
TestAttempt
,TestSummary
)
Beberapa dari acara ini ditampilkan di luar Bazel dalam
Protokol Peristiwa Build
(yakni BuildEvent
). Ini tidak hanya memungkinkan BlazeModule
, tetapi juga berbagai hal
di luar proses Bazel untuk
mengamati build. Dokumen tersebut dapat diakses sebagai
file yang berisi pesan protokol atau Bazel dapat
terhubung ke server (disebut
{i>Build Event Service<i}) untuk mengalirkan peristiwa.
Hal ini diterapkan di build.lib.buildeventservice
dan
build.lib.buildeventstream
paket Java.
Repositori eksternal
Sedangkan Bazel pada awalnya dirancang untuk digunakan dalam monorepo (satu sumber pohon yang berisi segala sesuatu yang dibutuhkan untuk membangun), Bazel tinggal di dunia di mana ini belum tentu benar. "Repositori eksternal" merupakan abstraksi yang digunakan untuk menjembatani dua dunia ini: mereka mewakili kode yang diperlukan untuk build tetapi tidak ada dalam hierarki sumber utama.
File WORKSPACE
Kumpulan repositori eksternal ditentukan dengan mengurai file WORKSPACE. Misalnya, deklarasi seperti ini:
local_repository(name="foo", path="/foo/bar")
Menghasilkan repositori bernama @foo
tersedia. Dari mana
rumit adalah Anda dapat menentukan
aturan repositori baru dalam file Starlark,
dapat digunakan untuk memuat kode Starlark baru, yang dapat digunakan untuk menentukan
aturan repositori dan sebagainya...
Untuk menangani kasus ini, penguraian file WORKSPACE (di
WorkspaceFileFunction
) dipecah menjadi beberapa bagian yang dipisahkan oleh load()
pernyataan pribadi Anda. Indeks potongan ditunjukkan oleh WorkspaceFileKey.getIndex()
dan
menghitung WorkspaceFileFunction
hingga indeks X berarti mengevaluasinya sampai
Pernyataan load()
ke-X.
Mengambil repositori
Sebelum kode repositori tersedia
untuk Bazel, kode itu perlu
diambil. Hal ini menyebabkan Bazel
membuat direktori di bawah
$OUTPUT_BASE/external/<repository name>
.
Mengambil repositori dilakukan dalam langkah-langkah berikut:
PackageLookupFunction
menyadari bahwa ia memerlukan repositori dan membuatRepositoryName
sebagaiSkyKey
, yang memanggilRepositoryLoaderFunction
RepositoryLoaderFunction
meneruskan permintaan keRepositoryDelegatorFunction
karena alasan yang tidak jelas (kode ini menyatakan bahwa menghindari mengunduh ulang sesuatu jika Skyframe dimulai ulang, tapi ini bukan alasan yang sangat kuat)RepositoryDelegatorFunction
menemukan aturan repositori yang diminta ambil dengan melakukan iterasi pada potongan file WORKSPACE sampai repositori ditemukanRepositoryFunction
yang sesuai ditemukan yang mengimplementasikan repositori fetching; bisa berupa implementasi Starlark dari repositori atau peta hard code untuk repositori yang diimplementasikan di Java.
Ada berbagai lapisan {i>caching<i} karena mengambil repositori bisa sangat mahal:
- Ada {i>cache<i} untuk file yang diunduh yang terkunci oleh {i>checksum<i}-nya
(
RepositoryCache
). Hal ini mengharuskan {i>checksum<i} tersedia di WORKSPACE, tapi itu tetap bagus untuk Hermeticity. Ini dibagikan oleh setiap {i>instance<i} server Bazel pada {i> workstation<i} yang sama, terlepas dari atau basis output tempat mereka berjalan. - "File penanda" ditulis untuk setiap repositori di
$OUTPUT_BASE/external
yang berisi {i>checksum<i} dari aturan yang digunakan untuk mengambilnya. Jika Bazel server dimulai ulang tetapi {i>checksum<i} tidak berubah, tidak diambil kembali. Ini diterapkan diRepositoryDelegatorFunction.DigestWriter
. - Opsi command line
--distdir
menetapkan cache lain yang digunakan untuk mencari artefak yang akan diunduh. Hal ini berguna dalam setelan perusahaan di mana Bazel tidak boleh mengambil hal acak dari Internet. Ini adalah yang diimplementasikan olehDownloadManager
.
Setelah repositori didownload, artefak di dalamnya diperlakukan sebagai sumber
artefak. Hal ini menimbulkan masalah karena Bazel biasanya memeriksa
artefak sumber dengan memanggil {i>stat()<i}, dan artefak ini juga
menjadi tidak valid ketika definisi repositori
yang sedang berubah menjadi tidak valid. Dengan demikian,
FileStateValue
untuk artefak dalam repositori eksternal yang perlu bergantung pada
repositori eksternal mereka. Hal ini ditangani oleh ExternalFilesHelper
.
Direktori terkelola
Terkadang, repositori eksternal perlu mengubah file di root ruang kerja (seperti pengelola paket yang menampung paket yang diunduh dalam subdirektori pohon sumber). Hal ini bertentangan dengan asumsi Bazel menyebutkan sumber tersebut file hanya dimodifikasi oleh pengguna dan tidak dengan sendirinya dan memungkinkan paket untuk merujuk ke setiap direktori pada {i>root <i}workspace. Untuk membuat komunikasi semacam ini repositori eksternal, Bazel melakukan dua hal:
- Memungkinkan pengguna untuk menentukan subdirektori ruang kerja yang tidak diizinkan Bazel
untuk dijangkau. Item tersebut tercantum dalam file bernama
.bazelignore
dan fungsi tersebut diimplementasikan diBlacklistedPackagePrefixesFunction
. - Kita mengenkode pemetaan dari subdirektori ruang kerja ke elemen eksternal
repositori yang ditangani ke dalam
ManagedDirectoriesKnowledge
FileStateValue
merujuk ke nama tersebut dengan cara yang sama seperti kata untuk repositori eksternal.
Pemetaan repositori
Bisa jadi beberapa repositori ingin
bergantung pada repositori yang sama,
tetapi dalam versi yang berbeda (ini adalah instance "dependensi berlian
tertentu"). Misalnya, jika dua biner dalam repositori terpisah dalam build
ingin bergantung pada Guava, mereka mungkin akan merujuk ke Guava dengan label
mulai @guava//
dan memperkirakannya akan memiliki versi yang berbeda.
Oleh karena itu, Bazel memungkinkan seseorang
memetakan ulang label repositori eksternal
string @guava//
dapat merujuk ke satu repositori Guava (seperti @guava1//
) di
repositori satu biner dan repositori Guava lainnya (seperti @guava2//
)
repositori sumber lain.
Atau, string ini juga dapat digunakan untuk menggabungkan berlian. Jika repositori
bergantung pada @guava1//
, dan yang lainnya bergantung pada @guava2//
, pemetaan repositori
memungkinkan seseorang memetakan ulang kedua repositori agar menggunakan repositori @guava//
kanonis.
Pemetaan ditetapkan di file WORKSPACE sebagai atribut repo_mapping
definisi repositori individual. Kemudian muncul di Skyframe sebagai anggota
WorkspaceFileValue
, yang ditautkan ke:
Package.Builder.repositoryMapping
yang digunakan untuk mengubah bernilai label atribut aturan dalam paket denganRuleClass.populateRuleAttributeValues()
Package.repositoryMapping
yang digunakan dalam fase analisis (untuk menyelesaikan hal-hal seperti$(location)
yang tidak diurai dalam pemuatan fase)BzlLoadFunction
untuk me-resolve label dalam pernyataan load()
Bit JNI
Server Bazel_ sebagian besar _ditulis dalam bahasa Java. Pengecualiannya adalah bagian-bagian yang Java tidak bisa melakukannya sendiri atau tidak bisa melakukannya sendiri ketika kita mengimplementasikannya. Ini sebagian besar terbatas pada interaksi dengan sistem file, kontrol proses dan berbagai hal tingkat rendah lainnya.
Kode C++ berada di bawah src/main/native dan class Java dengan metode tersebut adalah:
NativePosixFiles
danNativePosixFileSystem
ProcessUtils
WindowsFileOperations
danWindowsFileProcesses
com.google.devtools.build.lib.platform
Output konsol
Memancarkan {i>output<i} konsol tampak seperti hal yang sederhana, tetapi pertemuan banyak proses (kadang-kadang dari jarak jauh), {i>caching<i} yang sangat terperinci, keinginan untuk memiliki {i>output<i} terminal yang bagus dan penuh warna dan memiliki server yang berjalan lama membuat maka hal ini tidaklah sepele.
Tepat setelah panggilan RPC masuk dari klien, dua RpcOutputStream
dibuat (untuk {i>stdout<i} dan {i>stderr<i} yang meneruskan data yang dicetak ke dalam
mereka kepada klien. Objek ini kemudian digabungkan dalam OutErr
((stdout, stderr)
tertentu). Apa pun yang perlu dicetak
di konsol akan melalui proses
feed. Kemudian aliran data ini
diserahkan kepada
BlazeCommandDispatcher.execExclusively()
.
Output secara default dicetak dengan urutan escape ANSI. Bila ini bukan
diinginkan (--color=no
), dihapus oleh AnsiStrippingOutputStream
. Di beberapa
Selain itu, System.out
dan System.err
akan dialihkan ke aliran output ini.
Ini dimaksudkan agar informasi {i>debugging<i}
dapat dicetak menggunakan
System.err.println()
dan masih berakhir di output terminal klien
(yang berbeda dari server). Perlu diperhatikan
bahwa jika sebuah proses
menghasilkan output biner (seperti bazel query --output=proto
), tanpa munging stdout
berlangsung.
Pesan singkat (error, peringatan, dan sejenisnya) dinyatakan melalui
Antarmuka EventHandler
. Secara khusus, ini berbeda dari postingan
EventBus
(ini membingungkan). Setiap Event
memiliki EventKind
(error,
peringatan, info, dan beberapa lainnya) dan mereka mungkin memiliki Location
(tempat di
kode sumber yang menyebabkan peristiwa terjadi).
Beberapa implementasi EventHandler
menyimpan peristiwa yang diterimanya. Ini digunakan
memutar ulang informasi ke UI yang disebabkan
oleh berbagai jenis pemrosesan {i>cache<i},
misalnya, peringatan yang dikeluarkan oleh target yang dikonfigurasi dalam cache.
Beberapa EventHandler
juga mengizinkan postingan acara yang pada akhirnya akan ditemukan
bus peristiwa (Event
reguler _not _muncul di sana). Berikut adalah
implementasi ExtendedEventHandler
dan penggunaan utamanya adalah untuk memutar ulang cache
EventBus
peristiwa. Semua peristiwa EventBus
ini menerapkan Postable
, tetapi tidak
semua yang diposting ke EventBus
harus mengimplementasikan antarmuka ini;
hanya yang di-cache oleh ExtendedEventHandler
(akan lebih baik dan
yang biasanya
dilakukan; tapi tidak diterapkan)
Output terminal sebagian besar dipancarkan melalui UiEventHandler
, yang
bertanggung jawab atas semua pemformatan
{i>output<i} dan pelaporan kemajuan yang canggih,
fungsi tersebut. Kode ini memiliki dua input:
- {i>Bus<i} acara
- Aliran peristiwa ini disalurkan melalui Reporter
Satu-satunya koneksi langsung ke mesin eksekusi perintah (misalnya
Bazel) harus melakukan streaming RPC ke klien melalui Reporter.getOutErr()
,
yang memungkinkan akses
langsung ke aliran data ini. Ini hanya digunakan ketika
sebuah perintah membutuhkan
untuk membuang sejumlah besar kemungkinan data biner (seperti bazel query
).
Membuat Profil Bazel
Bazel adalah perangkat yang cepat. Bazel juga lambat, karena build cenderung tumbuh sampai
batas kemampuan AI
yang bisa ditanggung. Karena alasan ini, Bazel menyertakan
profiler yang dapat
digunakan untuk membuat profil
build dan Bazel itu sendiri. Hal ini diimplementasikan di class yang
dengan tepat diberi nama Profiler
. Ini diaktifkan secara default, meskipun hanya merekam
data yang diringkas sehingga {i>
overhead<i}-nya dapat ditoleransi; Command line
--record_full_profiler_data
membuatnya merekam semua hal yang bisa dilakukannya.
Alat ini memunculkan profil dalam format profiler Chrome; paling baik dilihat di Chrome. Model datanya adalah tumpukan tugas: seseorang dapat memulai tugas dan mengakhiri tugas dan mereka seharusnya bersarang dengan rapi satu sama lain. Setiap thread Java mendapatkan tumpukan tugasnya sendiri. TODO: Bagaimana cara kerjanya dengan tindakan dan gaya {i>continuation-passing<i}?
Profiler dimulai dan dihentikan di BlazeRuntime.initProfiler()
dan
BlazeRuntime.afterCommand()
masing-masing dan berupaya untuk aktif selama
mungkin sehingga kita
dapat membuat profil semuanya. Untuk menambahkan sesuatu ke profil,
panggil Profiler.instance().profile()
. Metode ini menampilkan Closeable
, yang penutupannya
mewakili akhir tugas. Opsi ini paling baik digunakan dengan try-with-resources
pernyataan pribadi Anda.
Kita juga melakukan pembuatan profil memori dasar di MemoryProfiler
. Fitur ini juga selalu aktif
dan kebanyakan mencatat ukuran heap maksimum dan perilaku GC.
Menguji Bazel
Bazel memiliki dua jenis pengujian utama: pengujian yang mengamati Bazel sebagai "kotak hitam" dan satu-satunya yang hanya menjalankan fase analisis. Kami menyebut yang sebelumnya "pengujian integrasi" dan "pengujian unit", meskipun lebih mirip dengan pengujian integrasi yang kurang terintegrasi. Kita juga memiliki beberapa pengujian unit yang sebenarnya, yang diperlukan.
Pengujian integrasi memiliki dua jenis:
- Yang diimplementasikan menggunakan kerangka kerja
pengujian bash yang sangat rumit
src/test/shell
- One diimplementasikan di Java. Ini diimplementasikan sebagai subclass
BuildIntegrationTestCase
BuildIntegrationTestCase
adalah framework pengujian integrasi pilihan karena
dapat digunakan dengan baik
untuk sebagian besar skenario pengujian. Karena ini adalah kerangka kerja Java,
menyediakan kemampuan debug dan integrasi yang lancar dengan banyak pengembangan umum
alat. Ada banyak contoh class BuildIntegrationTestCase
di
Repositori Bazel.
Pengujian analisis diterapkan sebagai subclass BuildViewTestCase
. Terdapat
sistem file awal yang dapat Anda gunakan untuk menulis file BUILD
, lalu berbagai helper
dapat meminta target yang dikonfigurasi, mengubah konfigurasi, dan menegaskan
berbagai hal tentang
hasil analisis.