Halaman ini membahas sistem build berbasis artefak dan filosofi di balik pembuatannya. Bazel adalah sistem build berbasis artefak. Meskipun sistem build berbasis tugas adalah langkah yang baik di atas skrip build, sistem ini memberikan terlalu banyak kekuatan kepada setiap engineer dengan mengizinkan mereka menentukan tugas mereka sendiri.
Sistem build berbasis artefak memiliki sejumlah kecil tugas yang ditentukan oleh sistem
yang dapat dikonfigurasi oleh engineer dengan cara terbatas. Engineer masih memberi tahu sistem
apa yang akan di-build, tetapi sistem build menentukan cara untuk mem-build-nya. Seperti
sistem build berbasis tugas, sistem build berbasis artefak, seperti Bazel, masih
memiliki buildfile, tetapi konten buildfile tersebut sangat berbeda. Buildfile di Bazel adalah manifes deklaratif yang menjelaskan kumpulan artefak yang akan dibuat, dependensinya, dan serangkaian opsi terbatas yang memengaruhi cara pembuatannya, bukan kumpulan perintah imperatif dalam bahasa skrip Turing-complete yang menjelaskan cara menghasilkan output. Saat engineer menjalankan bazel
di command line, mereka menentukan sekumpulan target untuk di-build (apa), dan
Bazel bertanggung jawab untuk mengonfigurasi, menjalankan, dan menjadwalkan langkah-langkah
kompilasi (bagaimana). Karena sistem build kini memiliki kontrol penuh atas alat
yang akan dijalankan, sistem ini dapat memberikan jaminan yang jauh lebih kuat sehingga memungkinkannya jauh
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. Sebaliknya, bahasa pemrograman fungsional (seperti Haskell dan ML) lebih terstruktur seperti serangkaian persamaan matematika. Dalam bahasa fungsional, programmer menjelaskan komputasi yang akan dilakukan, tetapi meninggalkan detail kapan dan persis bagaimana komputasi tersebut dieksekusi ke compiler.
Hal ini dipetakan ke ide mendeklarasikan manifes dalam sistem build berbasis artefak dan membiarkan sistem mencari tahu cara menjalankan build. Banyak masalah yang tidak dapat diekspresikan dengan mudah menggunakan pemrograman fungsional, tetapi masalah yang sangat mendapatkan manfaat darinya: bahasa ini sering kali dapat melakukan paralelisasi program tersebut secara sederhana dan memberikan jaminan yang kuat tentang kebenarannya yang tidak akan mungkin dalam bahasa imperatif. Masalah yang paling mudah diungkapkan menggunakan pemrograman fungsional adalah masalah yang hanya melibatkan transformasi satu bagian data menjadi bagian lain menggunakan serangkaian aturan atau fungsi. Dan itulah yang dimaksud dengan sistem build: seluruh sistem secara efektif merupakan fungsi matematika yang menggunakan file sumber (dan alat seperti compiler) sebagai input dan menghasilkan biner sebagai output. Jadi, tidak mengherankan jika hal ini berfungsi dengan baik untuk mendasarkan sistem build berdasarkan prinsip pemrograman fungsional.
Memahami sistem build berbasis artefak
Sistem build Google, Blaze, adalah sistem build berbasis artefak pertama. Bazel adalah versi open source dari Blaze.
Berikut tampilan file build (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",
],
)
Di 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 di command line dan oleh target lainsrcs
: file sumber yang akan dikompilasi untuk membuat artefak bagi targetdeps
: target lain yang harus dibuat sebelum target ini dan ditautkan ke target tersebut
Dependensi dapat berada dalam paket yang sama (seperti dependensi
MyBinary
pada :mylib
) atau pada paket lain dalam hierarki sumber yang sama
(seperti dependensi mylib
pada //java/com/example/common
).
Seperti sistem build berbasis tugas, Anda melakukan build menggunakan alat command line
Bazel. Untuk mem-build target MyBinary
, Anda menjalankan bazel build :MyBinary
. Setelah
memasukkan perintah tersebut untuk pertama kalinya di repositori bersih, Bazel:
- 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 target tersebut, secara rekursif. - Mem-build setiap dependensi tersebut secara berurutan. Bazel dimulai dengan mem-build setiap
target yang tidak memiliki dependensi lain dan melacak dependensi mana
yang masih perlu di-build untuk setiap target. Segera setelah semua dependensi
target di-build, Bazel akan mulai mem-build target tersebut. Proses ini
akan berlanjut hingga setiap dependensi transitif
MyBinary
telah di-build. - Mem-build
MyBinary
untuk menghasilkan biner akhir yang dapat dieksekusi yang menautkan semua dependensi yang di-build pada langkah 3.
Pada dasarnya, hal yang terjadi di sini mungkin tidak terlalu berbeda dengan yang terjadi saat menggunakan sistem build berbasis tugas. Memang, hasil akhirnya adalah biner yang sama, dan proses untuk membuatnya melibatkan analisis sekumpulan langkah untuk menemukan dependensi di antara keduanya, lalu menjalankan langkah-langkah tersebut secara berurutan. Namun, ada perbedaan penting. Yang pertama muncul di langkah 3: karena Bazel tahu bahwa setiap target hanya menghasilkan library Java, maka Bazel tahu bahwa yang harus dilakukan hanyalah menjalankan compiler Java, bukan skrip arbitrer yang ditentukan pengguna, sehingga Bazel tahu bahwa aman untuk menjalankan langkah-langkah ini secara paralel. Hal ini dapat menghasilkan peningkatan performa urutan magnitudo dibandingkan dengan membangun target satu per satu di mesin multicore, dan hanya dapat dilakukan karena pendekatan berbasis artefak membuat sistem build bertanggung jawab atas strategi eksekusi sendiri sehingga dapat memberikan jaminan yang lebih kuat tentang paralelisme.
Namun, manfaatnya melampaui paralelisme. Hal berikutnya yang diberikan
pendekatan ini menjadi jelas saat developer mengetik bazel
build :MyBinary
untuk kedua kalinya tanpa melakukan perubahan apa pun: Bazel keluar dalam waktu kurang
dari satu detik dengan pesan yang menyatakan bahwa target sudah yang terbaru. Hal ini
mungkin karena paradigma pemrograman fungsional yang kita bahas
sebelumnya—Bazel mengetahui bahwa setiap target hanya merupakan hasil dari menjalankan compiler
Java, dan mengetahui bahwa output dari compiler Java hanya bergantung pada
inputnya, sehingga selama input belum berubah, output dapat digunakan kembali.
Selain itu, analisis ini berfungsi di setiap tingkat; 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 mengetahui untuk mem-build ulang library tersebut,
mylib
, dan MyBinary
, tetapi menggunakan kembali //java/com/example/myproduct/otherlib
.
Karena Bazel mengetahui properti alat yang dijalankannya di setiap langkah,
alat ini hanya dapat mem-build ulang kumpulan artefak minimum setiap kali sekaligus
memastikan bahwa alat ini tidak akan menghasilkan build yang sudah tidak berlaku.
Memformat ulang proses build dalam hal artefak, bukan tugas, adalah hal yang halus tetapi efektif. Dengan mengurangi fleksibilitas yang ditampilkan kepada programmer, sistem build dapat mengetahui lebih lanjut apa yang dilakukan pada setiap langkah build. Alat ini dapat menggunakan pengetahuan ini untuk membuat build jauh lebih efisien dengan melakukan paralelisasi proses build dan menggunakan kembali output-nya. Namun, ini hanyalah langkah pertama, dan elemen penyusun paralelisme dan penggunaan kembali ini membentuk dasar untuk sistem build terdistribusi dan sangat skalabel.
Trik Bazel praktis lainnya
Sistem build berbasis artefak pada dasarnya menyelesaikan masalah paralelisme dan penggunaan kembali yang melekat dalam sistem build berbasis tugas. Namun, masih ada beberapa masalah yang muncul sebelumnya yang belum kami tangani. Bazel memiliki cara pintar untuk menyelesaikan setiap masalah ini, dan kita harus membahasnya sebelum melanjutkan.
Alat sebagai dependensi
Salah satu masalah yang kita temui sebelumnya adalah build bergantung pada alat yang diinstal di komputer kita, dan mereproduksi build di seluruh sistem mungkin sulit karena versi atau lokasi alat yang berbeda. Masalahnya menjadi lebih sulit saat project Anda menggunakan bahasa yang memerlukan alat yang berbeda berdasarkan platform tempatnya di-build atau dikompilasi (seperti, Windows versus Linux), dan setiap platform tersebut memerlukan serangkaian alat yang sedikit berbeda untuk melakukan tugas yang sama.
Bazel menyelesaikan bagian pertama masalah ini dengan memperlakukan alat sebagai dependensi untuk
setiap target. Setiap java_library
di ruang kerja secara implisit bergantung pada compiler
Java, yang secara default menggunakan compiler terkenal. Setiap kali mem-build
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 di-build ulang.
Bazel menyelesaikan bagian kedua masalah, independensi platform, dengan menetapkan konfigurasi build. Target tidak bergantung langsung pada alat, tetapi bergantung pada jenis konfigurasi:
- Konfigurasi host: alat build yang berjalan selama build
- Konfigurasi target: mem-build biner yang akhirnya Anda minta
Memperluas sistem build
Bazel dilengkapi dengan target untuk beberapa bahasa pemrograman populer secara langsung, tetapi para engineer akan selalu ingin melakukan lebih banyak hal—salah satu manfaat sistem berbasis tugas adalah fleksibilitasnya dalam mendukung semua jenis proses build, dan sebaiknya jangan mengabaikannya dalam sistem build berbasis artefak. Untungnya, Bazel memungkinkan jenis target yang didukungnya diperluas dengan menambahkan aturan kustom.
Untuk menentukan aturan di Bazel, penulis aturan mendeklarasikan input yang diperlukan 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 outputnya,
menjalankan file yang dapat dieksekusi tertentu atau menulis string tertentu ke file, dan dapat
terhubung ke tindakan lain melalui input dan outputnya. Artinya, tindakan
adalah unit composable tingkat terendah dalam sistem build—tindakan dapat melakukan
apa pun yang diinginkan selama hanya menggunakan input dan output yang dideklarasikan, dan
Bazel menangani penjadwalan tindakan dan meng-cache hasilnya sebagaimana mestinya.
Sistem ini tidak sepenuhnya aman karena tidak ada cara untuk menghentikan developer tindakan melakukan sesuatu seperti memperkenalkan proses nondeterministik sebagai bagian dari tindakan mereka. Namun, hal ini tidak sering terjadi dalam praktiknya, dan mendorong kemungkinan penyalahgunaan hingga ke tingkat tindakan akan sangat mengurangi peluang terjadinya error. Aturan yang mendukung banyak bahasa dan alat umum tersedia secara luas secara online, dan sebagian besar project tidak akan pernah perlu menentukan aturan mereka sendiri. Bahkan untuk aturan yang ada, definisi aturan hanya perlu ditentukan di satu tempat terpusat di repositori, yang berarti sebagian besar engineer akan dapat menggunakan aturan tersebut tanpa harus khawatir dengan penerapannya.
Mengisolasi lingkungan
Tindakan sepertinya mungkin mengalami masalah yang sama seperti tugas di sistem lain—bukankah masih mungkin untuk menulis tindakan yang menulis ke file yang sama dan akhirnya saling bertentangan? Sebenarnya, Bazel membuat konflik ini tidak mungkin terjadi dengan menggunakan sandboxing. Pada sistem yang didukung, setiap tindakan diisolasi dari setiap tindakan lain melalui sandbox sistem file. Secara efektif, setiap tindakan hanya dapat melihat tampilan sistem file yang dibatasi yang menyertakan input yang telah dideklarasikan dan output apa pun yang telah dihasilkannya. Hal ini diterapkan oleh sistem seperti LXC di Linux, teknologi yang sama di balik Docker. Artinya, tindakan tidak mungkin bertentangan satu sama lain karena tidak dapat membaca file apa pun yang tidak dideklarasikan, dan file apa pun yang ditulis tetapi tidak dideklarasikan akan dihapus saat tindakan selesai. Bazel juga menggunakan sandbox untuk membatasi tindakan agar tidak berkomunikasi melalui jaringan.
Membuat dependensi eksternal menjadi deterministik
Masih ada satu masalah yang tersisa: sistem build sering kali perlu mendownload
dependensi (baik alat maupun library) dari sumber eksternal, bukan
mem-build-nya 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 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 dalam kode sumber ruang kerja, hal ini juga dapat menyebabkan build yang tidak dapat direproduksi—build mungkin berfungsi pada suatu hari dan gagal pada hari berikutnya tanpa alasan yang jelas karena perubahan dependensi yang tidak diketahui. Terakhir, dependensi eksternal dapat menimbulkan risiko keamanan yang besar jika dimiliki oleh pihak ketiga: jika penyerang dapat menyusup ke server pihak ketiga tersebut, mereka dapat mengganti file dependensi dengan sesuatu dari desain mereka sendiri, yang berpotensi memberi mereka kontrol penuh atas lingkungan build dan output-nya.
Masalah mendasarnya adalah kita ingin sistem build mengetahui file ini tanpa harus memeriksanya ke dalam kontrol sumber. Memperbarui dependensi harus menjadi pilihan yang disengaja, tetapi pilihan tersebut harus dibuat sekali di tempat terpusat, bukan dikelola oleh setiap engineer atau secara otomatis oleh sistem. Hal ini karena meskipun dengan model “Live at Head”, kami tetap ingin build menjadi deterministik, yang menyiratkan bahwa jika Anda memeriksa commit dari minggu lalu, Anda akan melihat dependensi seperti yang ada saat itu, bukan seperti saat ini.
Bazel dan beberapa sistem build lainnya mengatasi masalah ini dengan mewajibkan file manifes seluruh ruang kerja yang mencantumkan hash kriptografis untuk setiap dependensi eksternal di ruang kerja. Hash adalah cara ringkas untuk merepresentasikan file secara unik tanpa memeriksa seluruh file ke dalam kontrol sumber. Setiap kali dependensi eksternal baru direferensikan dari ruang kerja, hash dependensi tersebut akan 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 dan mendownload ulang file hanya jika hash 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 diperiksa ke dalam kontrol sumber sebelum build akan menerima dependensi baru. Artinya, selalu ada catatan 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 akan menggunakan dependensi yang sama dengan yang digunakan pada saat versi tersebut diperiksa (atau akan gagal jika dependensi tersebut tidak lagi tersedia).
Tentu saja, hal ini masih dapat menjadi masalah jika server jarak jauh tidak tersedia atau mulai menayangkan data yang rusak—hal ini dapat menyebabkan semua build Anda mulai gagal jika Anda tidak memiliki salinan lain dari dependensi tersebut. Untuk menghindari masalah ini, sebaiknya, untuk project non-trivial, Anda mencerminkan semua dependensinya ke server atau layanan yang Anda percayai dan kontrol. Jika tidak, Anda akan selalu bergantung pada pihak ketiga untuk ketersediaan sistem build, meskipun hash yang di-check-in menjamin keamanannya.