Halaman ini membahas sistem build berbasis artefak dan filosofi di balik pembuatan konten. Bazel adalah sistem build berbasis artefak. Meskipun build berbasis tugas adalah langkah yang baik di atas skrip build, karena memberikan terlalu banyak kekuatan untuk insinyur individu dengan membiarkan mereka menentukan tugas mereka sendiri.
Sistem build berbasis artefak memiliki sejumlah kecil tugas yang ditentukan oleh sistem
yang dapat dikonfigurasi dengan cara terbatas oleh para insinyur/perekayasa. Insinyur tetap memberi tahu sistem
apa yang akan dibuat, tetapi sistem build menentukan cara membangunnya. Seperti halnya dengan
sistem build berbasis tugas, sistem build berbasis artefak, seperti Bazel,
memiliki {i>buildfile<i}, tetapi isi dari
{i>buildfile<i} tersebut sangat berbeda. Lebih suka
daripada menjadi seperangkat perintah penting
dalam bahasa skrip lengkap Turing
yang menjelaskan cara menghasilkan {i>output<i}, {i>
buildfile<i} di Bazel bersifat deklaratif
manifes yang menjelaskan sekumpulan artefak yang akan dibangun, dependensinya, dan
serangkaian opsi terbatas yang memengaruhi cara pembuatannya. Saat engineer menjalankan bazel
di command line, mereka menentukan kumpulan target yang akan dibangun (apa), dan
Bazel bertanggung jawab mengkonfigurasi, menjalankan, dan menjadwalkan kompilasi
langkah (bagaimana). Karena sistem build sekarang
memiliki kontrol penuh atas apa yang
{i>tool<i} untuk dijalankan kapan saja, maka dapat memberikan jaminan
yang jauh lebih kuat yang memungkinkannya untuk
lebih efisien, tetapi tetap menjamin ketepatannya.
Perspektif fungsional
Mudah untuk membuat analogi antara sistem build berbasis artefak dan fungsi pemrograman. Bahasa pemrograman imperatif tradisional (seperti, Java, C, dan Python) menentukan daftar pernyataan yang akan dieksekusi satu per satu, dalam dengan cara yang sama seperti sistem pembangunan berbasis tugas yang memungkinkan pemrogram mendefinisikan serangkaian langkah untuk melaksanakannya. Bahasa pemrograman fungsional (seperti, Haskell dan ML), di kontras, lebih terstruktur seperti serangkaian persamaan matematika. Di beberapa fungsional, {i>programmer<i} menggambarkan komputasi yang harus dilakukan, tetapi menyerahkan detail mengenai kapan dan bagaimana cara komputasi dijalankan ke compiler.
Hal ini dipetakan ke gagasan untuk mendeklarasikan manifes dalam sistem build berbasis artefak dan membiarkan sistem mengetahui cara mengeksekusi build. Banyak masalah tidak bisa diekspresikan dengan mudah menggunakan pemrograman fungsional, tetapi yang memberikan manfaat banyak dari hal tersebut: bahasa sering kali dapat dengan mudah memparalelkan seperti program dan membuat jaminan kuat tentang kebenarannya yang akan tidak mungkin dilakukan dalam bahasa imperatif. Masalah termudah untuk diekspresikan menggunakan pemrograman fungsional adalah yang hanya melibatkan transformasi satu bagian data ke dalam kelompok lain menggunakan serangkaian aturan atau fungsi. Dan persis apa itu sistem build: keseluruhan 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 ia berfungsi dengan baik untuk mendasarkan sistem di seputar prinsip pemrograman fungsional.
Memahami sistem build berbasis artefak
Sistem build Google, Blaze, adalah sistem build berbasis artefak pertama. Roti Bazel adalah Blaze versi {i>open source<i}.
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 {i>library<i} yang dapat digunakan oleh
biner atau library lainnya. Setiap target memiliki:
name
: cara target direferensikan pada command line dan oleh targetsrcs
: file sumber yang akan dikompilasi guna membuat artefak untuk targetdeps
: target lain yang harus dibangun sebelum target ini dan ditautkan ke dalam ini
Dependensi dapat berada dalam paket yang sama (seperti paket MyBinary
dependensi pada :mylib
) atau pada paket lain dalam hierarki sumber yang sama
(seperti dependensi mylib
pada //java/com/example/common
).
Seperti pada sistem build berbasis tugas, Anda
melakukan build menggunakan baris perintah Bazel
menyediakan alat command line gcloud. Untuk membuat target MyBinary
, Anda menjalankan bazel build :MyBinary
. Sesudah
memasukkan perintah itu untuk pertama
kali dalam 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
; sehingga adalah, setiap target yang menjadi dependensiMyBinary
dan setiap target yang target bergantung pada, secara rekursif. - Membangun setiap dependensi tersebut secara berurutan. Bazel mulai dengan
membuat masing-masing
target yang tidak memiliki dependensi lain dan melacak dependensi mana
masih perlu dibangun untuk setiap target. Segera setelah semua
dependensi dibangun, Bazel mulai membangun target itu. Proses ini
berlanjut sampai setiap dependensi transitif
MyBinary
dibuat. - Mem-build
MyBinary
untuk menghasilkan biner final yang dapat dieksekusi yang tertaut di semua dependensi yang dibuat pada langkah 3.
Pada dasarnya, mungkin tidak tampak seperti apa yang terjadi di sini adalah berbeda dari yang terjadi saat menggunakan sistem pembangunan berbasis tugas. Sebenarnya, hasil akhirnya adalah biner yang sama, dan proses untuk memproduksinya melibatkan menganalisis banyak langkah untuk menemukan dependensi di antara mereka, dan kemudian menjalankan langkah-langkah itu secara berurutan. Tetapi ada perbedaan kritis. Yang pertama muncul di langkah 3: karena Bazel tahu bahwa setiap target hanya menghasilkan {i>library<i} Java, tahu bahwa yang harus dilakukan hanyalah menjalankan Java compiler daripada skrip yang ditetapkan pengguna, sehingga tahu bahwa aman untuk menjalankan langkah-langkah ini secara paralel. Hal ini dapat menghasilkan peningkatan performa yang signifikan dibandingkan membangun menargetkan satu per satu pada mesin multicore, dan ini hanya dimungkinkan karena pendekatan berbasis artefak membiarkan sistem build bertanggung jawab atas eksekusinya sendiri strategi yang tepat sehingga dapat membuat jaminan yang lebih kuat tentang paralelisme.
Namun, manfaatnya melampaui paralelisme. Hal berikutnya setelah
terlihat jelas saat developer mengetik bazel
build :MyBinary
untuk kedua kalinya tanpa melakukan perubahan apa pun: Bazel keluar lebih lama
daripada detik dengan pesan yang menyatakan bahwa target sudah yang terbaru. Ini adalah
dimungkinkan karena paradigma
pemrograman fungsional yang telah kita bahas
Bazel tahu bahwa setiap target adalah hasil dari menjalankan Java
kompilator, dan mengetahui bahwa {i>output<i}
dari kompilator Java hanya bergantung pada
selama inputnya tidak berubah, outputnya dapat digunakan kembali.
Dan analisis ini bekerja di setiap level; jika MyBinary.java
berubah, Bazel akan tahu
untuk membangun ulang MyBinary
tetapi menggunakan kembali mylib
. Jika {i>file<i} sumber untuk
//java/com/example/common
berubah, Bazel tahu cara membuat ulang library itu,
mylib
, dan MyBinary
, tetapi gunakan kembali //java/com/example/myproduct/otherlib
.
Karena Bazel tahu tentang
properti alat yang dijalankannya di setiap langkah,
hanya mampu membangun kembali
kumpulan minimum artefak setiap kali
menjamin bahwa proses ini tidak akan menghasilkan build yang usang.
Membingkai ulang proses build dari segi artefak daripada tugas adalah hal yang kecil tapi canggih. Dengan mengurangi fleksibilitas yang diekspos ke programmer, build sistem dapat mengetahui lebih banyak tentang apa yang sedang dilakukan pada setiap langkah pembangunan. Teknologi ini dapat menggunakan pengetahuan ini untuk membuat build jauh lebih efisien dengan memparalelkan build proses dan menggunakan kembali {i>output-<i}nya. Tapi ini sebenarnya hanya langkah pertama, dan blok bangunan paralelisme ini dan penggunaan ulang menjadi dasar yang sangat skalabel.
Trik Bazel keren lainnya
Sistem build berbasis artefak pada dasarnya menyelesaikan masalah dengan paralelisme dan penggunaan ulang yang melekat dalam sistem build berbasis tugas. Tapi masih ada beberapa masalah yang muncul sebelumnya yang belum kita tangani. Bazel punya kecerdasan solusi untuk masalah ini, dan kita harus membahasnya sebelum melanjutkan.
Alat sebagai dependensi
Satu masalah yang kita temui sebelumnya adalah build bergantung pada alat yang diinstal dan mereproduksi {i>build <i} di seluruh sistem menjadi sulit karena versi atau lokasi alat yang berbeda. Masalahnya menjadi semakin sulit ketika proyek Anda menggunakan bahasa yang memerlukan {i>tool<i} berbeda berdasarkan platform yang sedang dibangun atau dikompilasi (seperti, Windows versus Linux), dan masing-masing platform tersebut membutuhkan seperangkat alat yang sedikit berbeda untuk melakukan pekerjaan yang sama.
Bazel mengatasi bagian pertama masalah ini
dengan memperlakukan {i>tool<i} sebagai dependensi untuk
setiap target. Setiap java_library
di ruang kerja secara implisit bergantung pada Java
{i>compiler<i}, yang secara {i>default<i}
adalah kompilator yang dikenal. Kapan pun Bazel membuat
java_library
, sistem akan memeriksa untuk memastikan bahwa compiler yang ditentukan tersedia
di lokasi yang diketahui. Sama seperti dependensi lainnya, jika compiler Java
perubahan, setiap artefak yang
bergantung padanya dibangun kembali.
Bazel memecahkan bagian kedua dari masalah, kemandirian platform, dengan menetapkan konfigurasi build yang benar. Bukan target bergantung langsung pada alat mereka, target tersebut bergantung pada jenis konfigurasi:
- Konfigurasi host: membuat alat yang berjalan selama build
- Konfigurasi target: membangun biner yang pada akhirnya Anda minta
Memperluas sistem build
Bazel target untuk beberapa bahasa pemrograman populer dari , tetapi para insinyur akan selalu ingin melakukan lebih banyak - bagian dari manfaat dari proyek berbasis tugas adalah fleksibilitasnya dalam mendukung segala jenis proses pembangunan, dan itu akan lebih baik untuk tidak menyerah dalam sistem build berbasis artefak. Untungnya, Bazel memungkinkan jenis target yang didukungnya untuk diperluas oleh menambahkan aturan kustom.
Untuk menentukan aturan di Bazel, penulis aturan mendeklarasikan input yang
diperlukan (dalam bentuk atribut yang diteruskan dalam file BUILD
) dan atribut
satu set output yang dihasilkan aturan. Penulis juga mendefinisikan tindakan yang
akan dihasilkan oleh aturan tersebut. Setiap tindakan mendeklarasikan
input dan {i>output-<i}nya,
menjalankan {i>executable<i} tertentu atau
menulis {i>string<i} tertentu ke file, dan dapat
yang terhubung ke tindakan lain
melalui input dan outputnya. Ini berarti bahwa tindakan
adalah unit composable level terendah dalam sistem build—sebuah tindakan dapat
apa pun yang diinginkannya selama hanya menggunakan input dan {i>output<i} yang dideklarasikannya, dan
Bazel menangani penjadwalan tindakan dan menyimpan hasilnya dalam cache yang sesuai.
Sistemnya tidak terjamin sepenuhnya mengingat tidak ada cara untuk menghentikan developer tindakan dari melakukan sesuatu seperti memperkenalkan proses nondeterministik sebagai bagian dari tindakan mereka. Namun, dalam praktiknya, hal ini jarang terjadi, kemungkinan terjadinya penyalahgunaan hingga ke tingkat tindakan berkurang secara signifikan peluang terjadinya kesalahan. Aturan yang mendukung banyak bahasa dan alat umum banyak tersedia secara {i>online<i}, dan sebagian besar proyek tidak perlu menentukan sendiri aturan. Bahkan bagi mereka yang memilikinya, definisi aturan hanya perlu didefinisikan dalam satu tempat terpusat di repositori, yang berarti sebagian besar insinyur akan dapat menggunakan aturan-aturan itu tanpa harus khawatir tentang implementasinya.
Mengisolasi lingkungan
Tindakan terdengar seperti mengalami masalah yang sama dengan sistem—apakah masih mungkin untuk menulis tindakan yang ditulis ke file dan akhirnya bertentangan satu sama lain? Sebenarnya, Bazel membuat ini konflik tidak mungkin terjadi dengan menggunakan sandbox. Aktif sistem, setiap tindakan diisolasi dari setiap tindakan lainnya melalui sistem file sandbox. Secara efektif, setiap tindakan hanya dapat melihat tampilan yang dibatasi dari sistem file yang menyertakan input yang telah dideklarasikannya dan setiap output yang dimilikinya diproduksi. Ini diberlakukan oleh sistem seperti LXC di Linux, teknologi yang sama di belakang Docker. Artinya, tidak mungkin suatu tindakan bertentangan dengan satu lagi karena mereka tidak dapat membaca file apa pun yang tidak mereka deklarasikan, dan setiap file yang mereka tulis tetapi tidak deklarasikan akan dibuang ketika tindakan hingga akhir. Bazel juga menggunakan {i>sandbox<i} untuk membatasi tindakan berkomunikasi melalui jaringan.
Membuat dependensi eksternal menjadi determenistik
Masih ada satu masalah yang tersisa: sistem build sering kali perlu mendownload
dependensi (apakah alat atau library) dari sumber eksternal, bukan
langsung membangunnya. Hal ini dapat dilihat pada 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, dan hal ini berpotensi mengharuskan sistem build untuk terus memeriksa apakah produk tersebut masih segar. Jika file jarak jauh berubah tanpa perubahan yang sesuai dalam kode sumber workspace, hal ini juga bisa menghasilkan build yang tidak dapat direproduksi mungkin bekerja suatu hari dan gagal di hari berikutnya tanpa alasan yang jelas karena perubahan dependensi. Terakhir, dependensi eksternal dapat menimbulkan risiko jika dimiliki oleh pihak ketiga: jika penyerang dapat menyusup server pihak ketiga itu, mereka dapat mengganti file dependensi dengan sesuatu dalam desain mereka sendiri, yang berpotensi memberi mereka kontrol penuh atas build Anda lingkungan dan output-nya.
Masalah mendasarnya adalah kita ingin sistem pembangunan mengetahui {i>file<i} tanpa harus memeriksa file itu ke dalam kontrol sumber. Mengupdate dependensi harus dilakukan secara sadar, tetapi pilihan itu harus dibuat sekali saja alih-alih dikelola oleh insinyur individu atau secara otomatis oleh sistem file. Hal ini karena meskipun dengan model “Live at Head”, kita masih menginginkan build bersifat determenistik, yang berarti bahwa jika Anda memeriksa commit dari minggu ini, Anda akan melihat dependensi seperti dahulu, bukan seperti sekarang.
Bazel dan beberapa sistem build lainnya mengatasi masalah ini dengan meminta file manifes ruang kerja yang mencantumkan hash kriptografi untuk setiap dependensi di ruang kerja. {i>Hash <i}adalah cara ringkas untuk merepresentasikan secara unik {i>file<i} tanpa memasukkan seluruh file ke dalam kontrol sumber. Setiap kali dependensi eksternal direferensikan dari ruang kerja, hash dependensi tersebut ditambahkan ke manifes, baik secara manual atau otomatis. Saat Bazel menjalankan , ia akan memeriksa hash sebenarnya dari dependensi yang di-cache terhadap yang ditentukan dalam manifes dan mendownload ulang file hanya jika hash-nya berbeda.
Jika artefak yang kita download memiliki hash yang berbeda dengan yang dideklarasikan dalam , build akan gagal kecuali jika hash dalam manifes diperbarui. Ini dapat dilakukan secara otomatis, tetapi perubahan itu harus disetujui dan diperiksa kontrol sumber sebelum build akan menerima dependensi baru. Hal ini berarti bahwa selalu ada catatan kapan dependensi diperbarui, dan dependensi tidak dapat diubah tanpa perubahan yang sesuai di sumber ruang kerja. Ini juga berarti bahwa, ketika memeriksa kode sumber versi lama, build dijamin menggunakan dependensi yang sama dengan yang digunakan pada titik kapan versi itu diperiksa (atau jika tidak, dependensi akan gagal jika tidak lagi tersedia).
Tentu saja, masih bisa menjadi masalah jika server jarak jauh menjadi tidak tersedia atau mulai menyajikan data yang rusak—hal ini dapat menyebabkan semua build Anda gagal jika Anda tidak memiliki salinan lain dari dependensi tersebut. Untuk menghindari hal ini masalah besar, kami menyarankan agar, untuk proyek yang tidak umum, Anda mencerminkan semua dependensi ke server atau layanan yang Anda percaya dan kendalikan. Jika tidak, Anda akan selalu tunduk kepada pihak ketiga atas ketersediaan, meskipun {i>hash<i} yang di{i>check-in<i} menjamin keamanannya.