Halaman ini membahas sistem build berbasis artefak dan filosofi di balik pembuatannya. Bazel adalah sistem build berbasis artefak. Meskipun sistem build berbasis tugas lebih baik dibandingkan skrip build, sistem build memberikan terlalu banyak kekuatan bagi setiap engineer dengan memungkinkan mereka menentukan tugas mereka sendiri.
Sistem build berbasis artefak memiliki sejumlah kecil tugas yang ditentukan oleh sistem
dan dapat dikonfigurasi secara terbatas oleh engineer. Engineer tetap memberi tahu sistem
apa yang harus dibangun, tetapi sistem build menentukan cara membangunnya. Seperti
sistem build berbasis tugas, sistem build berbasis artefak, seperti Bazel, masih
memiliki buildfile, tetapi konten buildfile tersebut sangat berbeda. Daripada
menjadi serangkaian perintah penting dalam bahasa skrip yang dilengkapi Turing
yang menjelaskan cara menghasilkan output, buildfile di Bazel adalah manifes
deklaratif yang menjelaskan serangkaian artefak untuk di-build, dependensinya, dan
serangkaian opsi terbatas yang memengaruhi cara pembuatannya. Saat menjalankan bazel
pada command line, mereka menentukan kumpulan target yang akan di-build (apa), dan
Bazel bertanggung jawab untuk mengonfigurasi, menjalankan, dan menjadwalkan langkah-langkah
kompilasi (bagaimana). Karena kini sistem build memiliki kontrol penuh atas
alat yang akan dijalankan kapan saja, sistem dapat memberikan jaminan yang jauh lebih kuat sehingga menjadi
lebih efisien sekaligus tetap menjamin ketepatan.
Perspektif fungsional
Sangat mudah untuk membuat analogi antara sistem build berbasis artefak dan pemrograman fungsional. Bahasa pemrograman imperatif tradisional (seperti, Java, C, dan Python) menentukan daftar pernyataan yang akan dieksekusi satu per satu, dengan cara yang sama seperti sistem build berbasis tugas yang memungkinkan programmer menentukan serangkaian langkah untuk dieksekusi. Secara kontras, bahasa pemrograman fungsional (misalnya Haskell dan ML) lebih terstruktur seperti serangkaian persamaan matematika. Dalam bahasa fungsional, programmer menjelaskan komputasi yang akan dijalankan, tetapi meninggalkan detail kapan dan tepatnya cara komputasi tersebut dijalankan ke compiler.
Hal ini dipetakan ke gagasan untuk mendeklarasikan manifes dalam sistem build berbasis artefak dan memungkinkan sistem menentukan cara menjalankan build tersebut. Banyak masalah tidak dapat dinyatakan dengan mudah menggunakan pemrograman fungsional, tetapi masalah yang sangat diuntungkan darinya: bahasa sering kali dapat dengan mudah memparalelkan program tersebut dan membuat jaminan kuat tentang keakuratannya yang tidak mungkin dalam bahasa imperatif. Masalah termudah untuk dinyatakan menggunakan pemrograman fungsional adalah masalah yang hanya melibatkan transformasi satu bagian data menjadi bagian data lainnya menggunakan serangkaian aturan atau fungsi. Dan itulah sistem build: secara efektif keseluruhan sistem adalah fungsi matematis yang menggunakan file sumber (dan alat seperti compiler) sebagai input dan menghasilkan biner sebagai output. Jadi, tidak mengherankan jika ia bekerja dengan baik untuk mendasarkan sistem build pada prinsip pemrograman fungsional.
Memahami sistem build berbasis artefak
Sistem build Google, Blaze, adalah sistem build berbasis artefak pertama. Bazel adalah versi Blaze open source.
Seperti inilah tampilan buildfile (biasanya bernama BUILD
) di Bazel:
java_binary(
name = "MyBinary",
srcs = ["MyBinary.java"],
deps = [
":mylib",
],
)
java_library(
name = "mylib",
srcs = ["MyLibrary.java", "MyHelper.java"],
visibility = ["//java/com/example/myproduct:__subpackages__"],
deps = [
"//java/com/example/common",
"//java/com/example/myproduct/otherlib",
],
)
Pada Bazel, file BUILD
menentukan target—dua jenis target di sini adalah
java_binary
dan java_library
. Setiap target sesuai dengan artefak yang
dapat dibuat oleh sistem: target biner menghasilkan biner yang dapat
dieksekusi secara langsung, dan target library menghasilkan library yang dapat digunakan oleh
biner atau library lainnya. Setiap target memiliki:
name
: cara target direferensikan pada command line dan oleh target lainsrcs
: file sumber yang akan dikompilasi guna membuat artefak untuk targetdeps
: target lain yang harus di-build sebelum target ini dan ditautkan ke dalamnya
Dependensi dapat berada dalam paket yang sama (seperti dependensi
MyBinary
pada :mylib
) atau pada paket yang berbeda dalam hierarki sumber yang sama
(seperti dependensi mylib
pada //java/com/example/common
).
Seperti pada sistem build berbasis tugas, Anda melakukan build menggunakan alat command line
Bazel. Untuk membuat target MyBinary
, Anda menjalankan bazel build :MyBinary
. Setelah
memasukkan perintah tersebut untuk pertama kalinya di repositori yang bersih, Bazel akan:
- Mengurai setiap file
BUILD
di ruang kerja untuk membuat grafik dependensi di antara artefak. - Menggunakan grafik untuk menentukan dependensi transitif
MyBinary
; yaitu, setiap target yang menjadi dependensiMyBinary
dan setiap target yang menjadi dependensi bagi target tersebut, secara berulang. - Membangun setiap dependensi tersebut secara berurutan. Bazel memulai dengan mem-build setiap
target yang tidak memiliki dependensi lain dan melacak dependensi mana yang masih
perlu dibangun untuk setiap target. Segera setelah semua dependensi
target dibangun, Bazel mulai membangun target tersebut. Proses ini
berlangsung sampai setiap dependensi transitif
MyBinary
di-build. - Mem-build
MyBinary
untuk menghasilkan biner final yang dapat dieksekusi yang menautkan semua dependensi yang dibangun pada langkah 3.
Pada dasarnya, yang terjadi di sini mungkin tidak terlihat jauh berbeda dengan yang terjadi saat menggunakan sistem build berbasis tugas. Hasil akhirnya adalah biner yang sama, dan proses untuk memproduksinya melibatkan analisis banyak langkah untuk menemukan dependensi di antara keduanya, lalu menjalankan langkah-langkah tersebut secara berurutan. Tetapi ada perbedaan kritis. Yang pertama muncul di langkah 3: karena Bazel mengetahui bahwa setiap target hanya menghasilkan library Java, Bazel mengetahui bahwa yang harus dilakukan hanyalah menjalankan compiler Java, bukan skrip arbitrer yang ditentukan pengguna, sehingga mengetahui bahwa aman untuk menjalankan langkah-langkah ini secara paralel. Hal ini dapat menghasilkan urutan peningkatan performa yang lebih baik dibandingkan membuat target satu per satu di mesin multicore, dan hanya dimungkinkan karena pendekatan berbasis artefak membiarkan sistem build bertanggung jawab atas strategi eksekusinya sendiri sehingga dapat membuat jaminan yang lebih kuat tentang paralelisme.
Namun, manfaatnya melampaui paralelisme. Hal berikutnya yang ditunjukkan
oleh pendekatan ini kepada kita akan terlihat saat developer mengetik bazel
build :MyBinary
untuk kedua kalinya tanpa membuat perubahan apa pun: Bazel keluar dalam waktu kurang dari
satu detik dengan pesan yang menyatakan bahwa target sudah yang terbaru. Hal ini
memungkinkan karena paradigma pemrograman fungsional yang telah kita bicarakan
sebelumnya—Bazel tahu bahwa setiap target hanya merupakan hasil dari menjalankan compiler Java, dan mengetahui bahwa output dari compiler Java hanya bergantung pada
inputnya, jadi selama inputnya tidak berubah, output dapat digunakan kembali.
Analisis ini berfungsi di setiap level; jika MyBinary.java
berubah, Bazel akan
mengetahui cara mem-build ulang MyBinary
, tetapi menggunakan kembali mylib
. Jika file sumber untuk
//java/com/example/common
berubah, Bazel akan tahu cara membuat ulang library tersebut,
mylib
, dan MyBinary
, tetapi menggunakan kembali //java/com/example/myproduct/otherlib
.
Karena mengetahui properti alat yang dijalankan di setiap langkah,
Bazel hanya dapat membangun ulang kumpulan artefak minimum setiap saat,
sekaligus menjamin bahwa build tersebut tidak akan menghasilkan build yang usang.
Membingkai ulang proses build dari segi artefak, bukan tugas, adalah hal yang kecil tetapi ampuh. Dengan mengurangi fleksibilitas yang diekspos kepada programmer, sistem build dapat mengetahui lebih banyak tentang apa yang sedang dilakukan pada setiap langkah build. AI dapat menggunakan pengetahuan ini untuk membuat build jauh lebih efisien dengan memparalelkan proses build dan menggunakan kembali outputnya. Namun, ini hanyalah langkah pertama, dan elemen penyusun paralelisme dan penggunaan kembali ini menjadi dasar untuk sistem build yang terdistribusi dan sangat skalabel.
Trik Bazel keren lainnya
Sistem build berbasis artefak pada dasarnya menyelesaikan masalah dengan paralelisme dan penggunaan kembali yang melekat dalam sistem build berbasis tugas. Tapi masih ada beberapa masalah yang muncul sebelumnya yang belum kita tangani. Bazel memiliki cara cerdas untuk menyelesaikan setiap masalah ini, dan kita harus mendiskusikannya sebelum melanjutkan.
Alat sebagai dependensi
Satu masalah yang kita hadapi sebelumnya adalah build bergantung pada alat yang diinstal di mesin kita, dan mereproduksi build di seluruh sistem bisa menjadi sulit karena versi alat atau lokasi yang berbeda. Masalah ini menjadi lebih sulit saat project Anda menggunakan bahasa yang memerlukan alat berbeda berdasarkan platform tempat bahasa tersebut dibangun atau dikompilasi (seperti, Windows versus Linux), dan masing-masing platform tersebut memerlukan set alat yang sedikit berbeda untuk melakukan tugas yang sama.
Bazel mengatasi bagian pertama masalah ini dengan memperlakukan alat sebagai dependensi pada
setiap target. Setiap java_library
di ruang kerja secara implisit bergantung pada compiler Java, yang secara default menggunakan compiler populer. Setiap kali Bazel membangun
java_library
, Bazel akan memeriksa untuk memastikan bahwa compiler yang ditentukan tersedia
di lokasi yang diketahui. Sama seperti dependensi lainnya, jika compiler Java
berubah, setiap artefak yang bergantung padanya akan dibuat ulang.
Bazel mengatasi bagian kedua masalah, yakni independensi platform, dengan menyetel konfigurasi build. Target bergantung pada jenis konfigurasi, bukan target yang bergantung langsung pada alatnya:
- Konfigurasi host: membuat alat yang berjalan selama build
- Konfigurasi target: membangun biner yang pada akhirnya Anda minta
Memperluas sistem build
Bazel memiliki target untuk beberapa bahasa pemrograman populer yang siap pakai, tetapi engineer akan selalu ingin melakukan lebih banyak hal—bagian dari manfaat sistem berbasis tugas adalah fleksibilitasnya dalam mendukung segala jenis proses build, dan akan lebih baik untuk tidak melepaskannya dalam sistem build berbasis artefak. Untungnya, Bazel memungkinkan jenis target yang didukungnya diperpanjang dengan menambahkan aturan kustom.
Untuk menentukan aturan di Bazel, pembuat aturan mendeklarasikan input yang diperlukan oleh aturan (dalam bentuk atribut yang diteruskan dalam file BUILD
) dan kumpulan output tetap yang dihasilkan aturan. Penulis juga menentukan tindakan yang
akan dihasilkan oleh aturan tersebut. Setiap tindakan mendeklarasikan input dan output-nya,
menjalankan file tertentu yang dapat dieksekusi atau menulis string tertentu ke file, dan dapat
dihubungkan ke tindakan lain melalui input dan outputnya. Ini berarti tindakan
adalah unit composable tingkat terendah dalam sistem build—tindakan dapat melakukan
apa pun yang diinginkan selama hanya menggunakan input dan output yang dideklarasikannya, dan
Bazel menangani tindakan penjadwalan dan menyimpan hasilnya dalam cache yang sesuai.
Sistem ini tidak terjamin sepenuhnya mengingat tidak ada cara untuk menghentikan developer tindakan melakukan sesuatu seperti memperkenalkan proses nondeterministik sebagai bagian dari tindakan mereka. Namun, hal ini jarang terjadi dalam praktiknya, dan mendorong kemungkinan penyalahgunaan hingga ke tingkat tindakan akan sangat mengurangi peluang error. Aturan yang mendukung banyak bahasa dan alat umum tersedia secara luas secara online, dan sebagian besar project tidak perlu menetapkan aturannya sendiri. Bahkan bagi perusahaan yang menggunakannya, definisi aturan hanya perlu ditentukan di satu tempat terpusat di repositori, yang berarti sebagian besar engineer akan dapat menggunakan aturan tersebut tanpa perlu mengkhawatirkan implementasinya.
Mengisolasi lingkungan
Tindakan terdengar seperti dapat mengalami masalah yang sama dengan tugas di sistem lain—apakah masih mungkin untuk menulis tindakan yang keduanya menulis ke file yang sama dan akhirnya saling bertentangan? Sebenarnya, Bazel membuat konflik ini tidak mungkin dilakukan dengan menggunakan sandboxing. Pada sistem yang didukung, setiap tindakan diisolasi dari setiap tindakan lainnya melalui sandbox sistem file. Secara efektif, setiap tindakan hanya dapat melihat tampilan terbatas dari sistem file yang menyertakan input yang telah dideklarasikannya dan output apa pun yang dihasilkannya. Hal ini diterapkan oleh sistem seperti LXC di Linux, teknologi yang sama di balik Docker. Artinya, tidak mungkin suatu tindakan bertentangan satu sama lain karena tindakan tersebut tidak dapat membaca file yang tidak dideklarasikannya, dan setiap file yang ditulis tetapi tidak dideklarasikan akan dibuang saat tindakan selesai. Bazel juga menggunakan sandbox untuk membatasi tindakan agar tidak berkomunikasi melalui jaringan.
Membuat dependensi eksternal menjadi determenistik
Masih ada satu masalah yang tersisa: sistem build sering kali harus mendownload
dependensi (baik alat maupun library) dari sumber eksternal, bukan
membangunnya secara langsung. Hal ini dapat dilihat dalam contoh melalui
dependensi @com_google_common_guava_guava//jar
, yang mendownload file JAR
dari Maven.
Bergantung pada file di luar ruang kerja saat ini sangat berisiko. File tersebut dapat berubah kapan saja, yang berpotensi mengharuskan sistem build untuk terus memeriksa apakah file tersebut baru. Jika file jarak jauh berubah tanpa perubahan yang sesuai pada kode sumber Workspace, hal tersebut juga dapat menyebabkan build yang tidak dapat direproduksi—build mungkin berfungsi pada suatu hari dan gagal pada langkah berikutnya tanpa alasan yang jelas karena perubahan dependensi yang tidak diketahui. Terakhir, dependensi eksternal dapat menimbulkan risiko keamanan yang sangat besar jika dimiliki oleh pihak ketiga: jika penyerang dapat menyusup ke server pihak ketiga tersebut, ia dapat mengganti file dependensi dengan sesuatu dari desainnya sendiri, sehingga berpotensi memberinya kontrol penuh atas lingkungan build Anda dan output-nya.
Masalah dasarnya adalah kita ingin sistem build mengetahui file ini tanpa harus memeriksanya ke kontrol sumber. Memperbarui dependensi harus dilakukan secara sadar, tetapi pilihan tersebut harus dibuat sekali di tempat pusat, bukan dikelola oleh tiap engineer atau secara otomatis oleh sistem. Hal ini karena meskipun dengan model “Live at Head”, kita masih ingin build bersifat determenistik, yang menyiratkan bahwa jika Anda memeriksa commit dari minggu lalu, Anda akan melihat dependensi seperti sebelumnya, bukan seperti sekarang.
Bazel dan beberapa sistem build lainnya mengatasi masalah ini dengan memerlukan file manifes seluruh ruang kerja yang mencantumkan hash kriptografi untuk setiap dependensi eksternal di ruang kerja. Hash adalah cara ringkas untuk mewakili file secara unik tanpa harus memeriksa seluruh file ke kontrol sumber. Setiap kali dependensi eksternal baru direferensikan dari ruang kerja, hash dependensi tersebut ditambahkan ke manifes, baik secara manual maupun otomatis. Saat menjalankan build, Bazel akan memeriksa hash sebenarnya dari dependensi yang di-cache terhadap hash yang diharapkan yang ditentukan dalam manifes, lalu mendownload ulang file tersebut hanya jika hash-nya berbeda.
Jika artefak yang kita download memiliki hash yang berbeda dengan yang dideklarasikan dalam manifes, build akan gagal kecuali jika hash dalam manifes diperbarui. Hal ini dapat dilakukan secara otomatis, tetapi perubahan tersebut harus disetujui dan dimasukkan ke kontrol sumber sebelum build menerima dependensi baru. Artinya, selalu ada data tentang kapan dependensi diperbarui, dan dependensi eksternal tidak dapat berubah tanpa perubahan yang sesuai di sumber ruang kerja. Hal ini juga berarti bahwa, saat memeriksa kode sumber versi lama, build dijamin menggunakan dependensi yang sama dengan yang digunakan pada saat versi tersebut di-check in (atau jika tidak, akan gagal jika dependensi tersebut tidak lagi tersedia).
Tentu saja, hal ini masih dapat menjadi masalah jika server jarak jauh menjadi tidak tersedia atau mulai menyajikan data yang rusak. Hal ini dapat menyebabkan semua build Anda mulai gagal jika tidak tersedia salinan lain dari dependensi tersebut. Untuk menghindari masalah ini, sebaiknya, untuk project yang tidak umum, Anda mencerminkan semua dependensinya ke server atau layanan yang Anda percayai dan kontrol. Jika tidak, Anda akan selalu bergantung pada pihak ketiga atas ketersediaan sistem build, meskipun hash yang telah check-in menjamin keamanannya.