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 lebih baik daripada skrip build, sistem ini memberikan terlalu banyak kemampuan kepada masing-masing 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 tetap memberi tahu sistem apa yang akan di-build, tetapi sistem build menentukan cara mem-build-nya. Seperti sistem build berbasis tugas, sistem build berbasis artefak, seperti Bazel, masih memiliki file build, tetapi konten file build tersebut sangat berbeda. Daripada
menjadi serangkaian perintah imperatif dalam bahasa skrip Turing-lengkap
yang menjelaskan cara menghasilkan output, file build di Bazel adalah manifes
deklaratif yang menjelaskan serangkaian artefak yang akan dibangun, dependensinya, dan
serangkaian opsi terbatas yang memengaruhi cara artefak tersebut dibangun. Saat menjalankan bazel
di command line, engineer menentukan serangkaian target yang akan dibangun (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 dan kapan, sistem ini dapat memberikan jaminan yang jauh lebih kuat sehingga jauh lebih efisien sekaligus tetap menjamin kebenaran.
Perspektif fungsional
Membuat analogi antara sistem build berbasis artefak dan pemrograman fungsional sangatlah mudah. Bahasa pemrograman imperatif tradisional (seperti Java, C, dan Python) menentukan daftar pernyataan yang akan dieksekusi satu demi satu, dengan cara yang sama seperti sistem build berbasis tugas memungkinkan programmer menentukan serangkaian langkah yang akan dieksekusi. Bahasa pemrograman fungsional (seperti, Haskell dan ML), sebaliknya, disusun lebih seperti serangkaian persamaan matematika. Dalam bahasa fungsional, programmer menjelaskan komputasi yang akan dilakukan, tetapi menyerahkan detail kapan dan bagaimana tepatnya komputasi tersebut dieksekusi kepada compiler.
Hal ini sesuai dengan ide mendeklarasikan manifes dalam sistem build berbasis artefak dan membiarkan sistem menentukan cara menjalankan build. Banyak masalah yang tidak dapat dinyatakan dengan mudah menggunakan pemrograman fungsional, tetapi masalah yang dapat dinyatakan dengan mudah akan sangat diuntungkan: bahasa ini sering kali dapat memparalelkan program tersebut dengan mudah dan memberikan jaminan kuat tentang kebenarannya yang tidak mungkin dilakukan dalam bahasa imperatif. Masalah yang paling mudah diekspresikan menggunakan pemrograman fungsional adalah masalah yang hanya melibatkan transformasi satu bagian data menjadi bagian data lain menggunakan serangkaian aturan atau fungsi. Dan itulah yang dimaksud dengan sistem build: seluruh sistem secara efektif merupakan fungsi matematika yang mengambil file sumber (dan alat seperti compiler) sebagai input dan menghasilkan biner sebagai output. Jadi, tidak mengherankan jika sistem build yang didasarkan pada prinsip-prinsip pemrograman fungsional berfungsi dengan baik.
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 lain. Setiap target memiliki:
name
: cara target dirujuk di command line dan oleh target lainnyasrcs
: file sumber yang akan dikompilasi untuk membuat artefak bagi targetdeps
: target lain yang harus dibangun sebelum target ini dan ditautkan ke target ini
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 sistem build berbasis tugas, Anda melakukan build menggunakan alat command line Bazel. Untuk membuat target MyBinary
, jalankan bazel build :MyBinary
. Setelah
memasukkan perintah tersebut untuk pertama kalinya di repositori yang 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 bergantung padaMyBinary
dan setiap target yang bergantung pada target tersebut secara rekursif. - Membangun setiap dependensi tersebut secara berurutan. Bazel dimulai dengan membangun setiap
target yang tidak memiliki dependensi lain dan melacak dependensi mana yang
masih perlu dibangun untuk setiap target. Segera setelah semua dependensi target
dibuat, Bazel mulai membuat target tersebut. Proses ini
berlanjut hingga setiap dependensi transitif
MyBinary
telah dibuat. - Membangun
MyBinary
untuk menghasilkan biner yang dapat dieksekusi akhir yang menautkan semua dependensi yang dibangun pada langkah 3.
Pada dasarnya, apa yang terjadi di sini mungkin tidak jauh berbeda dengan apa yang terjadi saat menggunakan sistem build berbasis tugas. Memang, hasil akhirnya adalah biner yang sama, dan proses pembuatannya melibatkan analisis sejumlah langkah untuk menemukan dependensi di antara langkah-langkah tersebut, 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, Bazel tahu bahwa yang harus dilakukan hanyalah menjalankan kompilator Java, bukan skrip arbitrer yang ditentukan pengguna, sehingga Bazel tahu bahwa langkah-langkah ini aman untuk dijalankan secara paralel. Hal ini dapat menghasilkan peningkatan performa yang signifikan dibandingkan dengan membangun target satu per satu di mesin multi-core, dan hanya dapat dilakukan karena pendekatan berbasis artefak membuat sistem build bertanggung jawab atas strategi eksekusinya sendiri sehingga dapat memberikan jaminan yang lebih kuat tentang paralelisme.
Namun, manfaatnya tidak hanya terbatas pada 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 terbaru. Hal ini
dapat dilakukan karena paradigma pemrograman fungsional yang kita bahas
sebelumnya—Bazel tahu bahwa setiap target adalah hasil dari menjalankan compiler Java, dan Bazel tahu bahwa output dari compiler Java hanya bergantung pada
inputnya, jadi selama inputnya tidak berubah, outputnya dapat digunakan kembali.
Analisis ini berfungsi di setiap tingkat; jika MyBinary.java
berubah, Bazel tahu
untuk membangun ulang MyBinary
, tetapi menggunakan kembali mylib
. Jika file sumber untuk
//java/com/example/common
berubah, Bazel tahu harus membangun kembali library tersebut,
mylib
, dan MyBinary
, tetapi menggunakan kembali //java/com/example/myproduct/otherlib
.
Karena Bazel mengetahui properti alat yang dijalankannya di setiap langkah, Bazel dapat membangun ulang hanya set artefak minimum setiap kali sambil menjamin bahwa Bazel tidak akan menghasilkan build yang tidak berlaku.
Mengubah kerangka proses build dalam hal artefak, bukan tugas, memang tidak terlalu terlihat, tetapi sangat efektif. Dengan mengurangi fleksibilitas yang diekspos ke programmer, sistem build dapat mengetahui lebih banyak tentang apa yang dilakukan di setiap langkah build. Build 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 ulang ini membentuk dasar untuk sistem build terdistribusi dan sangat skalabel.
Trik Bazel keren lainnya
Sistem build berbasis artefak pada dasarnya memecahkan masalah paralelisme dan penggunaan ulang yang melekat pada sistem build berbasis tugas. Namun, masih ada beberapa masalah yang muncul sebelumnya yang belum kita tangani. Bazel memiliki cara cerdas untuk menyelesaikan setiap masalah ini, dan kita harus membahasnya sebelum melanjutkan.
Alat sebagai dependensi
Salah satu masalah yang kami alami sebelumnya adalah build bergantung pada alat yang diinstal di komputer kami, dan mereproduksi build di berbagai sistem bisa jadi sulit karena versi atau lokasi alat yang berbeda. Masalah ini menjadi lebih sulit jika project Anda menggunakan bahasa yang memerlukan alat yang berbeda berdasarkan platform tempat bahasa tersebut dibangun atau dikompilasi (seperti, Windows versus Linux), dan setiap platform tersebut memerlukan serangkaian alat yang sedikit berbeda untuk melakukan tugas yang sama.
Bazel memecahkan 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 adalah compiler terkenal. 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 dibangun ulang.
Bazel memecahkan bagian kedua masalah, yaitu independensi platform, dengan menetapkan konfigurasi build. Daripada target bergantung langsung pada alatnya, target bergantung pada jenis konfigurasi:
- Konfigurasi host: alat build yang berjalan selama build
- Konfigurasi target: membangun biner yang pada akhirnya Anda minta
Memperluas sistem build
Bazel dilengkapi dengan target untuk beberapa bahasa pemrograman populer secara langsung, tetapi engineer akan selalu ingin melakukan lebih banyak hal—bagian dari manfaat sistem berbasis tugas adalah fleksibilitasnya dalam mendukung semua jenis proses build, dan akan lebih baik untuk tidak menyerahkannya 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 menyatakan input dan outputnya,
menjalankan file yang dapat dieksekusi tertentu atau menulis string tertentu ke file, dan dapat
dihubungkan ke tindakan lain melalui input dan outputnya. Artinya, tindakan
adalah unit composable tingkat terendah dalam sistem build—tindakan dapat melakukan
apa pun yang diinginkannya selama hanya menggunakan input dan output yang dideklarasikan, dan
Bazel akan menangani penjadwalan tindakan dan menyimpan hasilnya dalam cache sebagaimana mestinya.
Sistem ini tidak sepenuhnya aman karena tidak ada cara untuk menghentikan developer tindakan melakukan sesuatu seperti memperkenalkan proses non-deterministik sebagai bagian dari tindakan mereka. Namun, hal ini jarang terjadi dalam praktiknya, dan mendorong kemungkinan penyalahgunaan hingga ke tingkat tindakan akan sangat mengurangi peluang terjadinya kesalahan. Aturan yang mendukung banyak bahasa dan alat umum tersedia secara luas di internet, dan sebagian besar project tidak perlu menentukan aturan mereka sendiri. Bahkan untuk yang melakukannya, definisi aturan hanya perlu ditentukan di satu tempat pusat di repositori, yang berarti sebagian besar engineer akan dapat menggunakan aturan tersebut tanpa perlu mengkhawatirkan penerapannya.
Mengisolasi lingkungan
Tindakan tampaknya dapat 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 sandbox. 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 mencakup input yang telah dideklarasikan dan output yang telah dihasilkan. Hal ini diterapkan oleh sistem seperti LXC di Linux, teknologi yang sama di balik Docker. Artinya, tindakan tidak mungkin bertentangan satu sama lain karena tindakan tidak dapat membaca file yang tidak dideklarasikan, dan file 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
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 berisiko. File tersebut dapat berubah kapan saja, sehingga sistem build harus terus memeriksa apakah file tersebut sudah terbaru. 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 disadari. Terakhir, dependensi eksternal dapat menimbulkan risiko keamanan yang sangat besar jika dimiliki oleh pihak ketiga: jika penyerang dapat menyusup ke server pihak ketiga tersebut, mereka dapat mengganti file dependensi dengan sesuatu yang mereka rancang sendiri, yang berpotensi memberi mereka kontrol penuh atas lingkungan build dan outputnya.
Masalah mendasarnya adalah kita ingin sistem build mengetahui file-file ini tanpa harus memeriksanya ke kontrol sumber. Memperbarui dependensi harus menjadi pilihan yang disadari, tetapi pilihan tersebut harus dibuat sekali di tempat pusat, bukan dikelola oleh masing-masing engineer atau secara otomatis oleh sistem. Hal ini karena meskipun dengan model “Live at Head”, kami tetap ingin build bersifat deterministik, yang berarti jika Anda memeriksa commit dari minggu lalu, Anda akan melihat dependensi seperti saat itu, bukan seperti sekarang.
Bazel dan beberapa sistem build lainnya mengatasi masalah ini dengan mewajibkan file manifes di seluruh ruang kerja yang mencantumkan hash kriptografi 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 dirujuk 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 dan mendownload ulang file hanya jika hash berbeda.
Jika artefak yang kita download memiliki hash yang berbeda dengan yang dideklarasikan dalam manifest, build akan gagal kecuali hash dalam manifest 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 tentang kapan dependensi diperbarui, dan dependensi eksternal tidak dapat berubah tanpa perubahan yang sesuai di sumber ruang kerja. Ini juga berarti bahwa, saat memeriksa versi kode sumber yang lebih lama, build dijamin akan menggunakan dependensi yang sama dengan yang digunakannya pada saat versi tersebut diperiksa (atau akan gagal jika dependensi tersebut tidak lagi tersedia).
Tentu saja, hal ini tetap dapat menjadi masalah jika server jarak jauh tidak tersedia atau mulai menyajikan data yang rusak—hal ini dapat menyebabkan semua build Anda mulai gagal jika Anda tidak memiliki salinan dependensi lain yang tersedia. Untuk menghindari masalah ini, sebaiknya, untuk setiap project yang tidak sepele, 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 Anda, meskipun hash yang di-check-in menjamin keamanannya.