Sistem Build Berbasis Artefak

Laporkan masalah Lihat sumber

Halaman ini membahas sistem build berbasis artefak dan filosofi di balik pembuatannya. Bazel adalah sistem build berbasis artefak. Meskipun sistem build berbasis tugas merupakan langkah tepat di atas skrip build, sistem ini memberikan terlalu banyak kemampuan kepada setiap engineer dengan memungkinkannya menentukan tugasnya sendiri.

Sistem build berbasis artefak memiliki sedikit tugas yang ditentukan oleh sistem yang dapat dikonfigurasi oleh engineer dengan cara terbatas. Engineer masih memberi tahu sistem apa yang harus di-build, tetapi sistem build menentukan cara mem-build-nya. Seperti sistem build berbasis tugas, sistem build berbasis artefak, seperti Bazel, masih memiliki buildfile, tetapi isi buildfile tersebut sangat berbeda. Bukan merupakan sekumpulan perintah imperatif dalam bahasa skrip lengkap Turing yang menjelaskan cara menghasilkan output, buildfile di Bazel adalah manifes deklaratif yang menjelaskan kumpulan artefak yang akan di-build, dependensinya, dan kumpulan opsi terbatas yang memengaruhi cara artefak tersebut di-build. Saat menjalankan bazel pada command line, engineer menentukan kumpulan target yang akan di-build (apa) dan Bazel bertanggung jawab untuk mengonfigurasi, menjalankan, dan menjadwalkan langkah-langkah kompilasi (caranya). Karena sistem build kini memiliki kontrol penuh atas alat yang akan dijalankan, sistem build dapat membuat jaminan yang jauh lebih kuat yang memungkinkannya menjadi jauh lebih efisien sambil tetap menjamin kebenaran.

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 dijalankan 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), terstruktur lebih mirip dengan serangkaian persamaan matematika. Dalam bahasa fungsional, programmer menjelaskan komputasi yang akan dilakukan, tetapi meninggalkan detail kapan dan bagaimana tepatnya komputasi tersebut dieksekusi ke compiler.

Hal ini dipetakan ke ide mendeklarasikan manifes dalam sistem build berbasis artefak dan memungkinkan sistem mengetahui cara mengeksekusi build. Banyak masalah tidak dapat dinyatakan dengan mudah menggunakan pemrograman fungsional, tetapi masalah yang sangat diuntungkan dari bahasa tersebut: bahasa sering kali dapat dengan mudah memparalelkan program tersebut dan membuat jaminan kuat tentang kebenaran mereka yang tidak mungkin dilakukan dalam bahasa imperatif. Masalah termudah untuk diekspresikan menggunakan pemrograman fungsional adalah masalah yang hanya melibatkan transformasi satu bagian data ke bagian lainnya menggunakan serangkaian aturan atau fungsi. Dan itulah sistem build sebenarnya: seluruh sistem sebenarnya adalah fungsi matematika yang menggunakan file sumber (dan alat seperti compiler) sebagai input dan menghasilkan biner sebagai output. Jadi, tidak mengherankan bahwa pengujian ini berfungsi dengan baik untuk mendasarkan sistem pada prinsip-prinsip pemrograman fungsional.

Memahami sistem build berbasis artefak

Sistem build Google, Blaze, adalah sistem build berbasis artefak pertama. Bazel adalah versi Blaze open source.

Berikut adalah 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",
    ],
)

Di Bazel, file BUILD menentukan target—dua jenis target di sini adalah java_binary dan java_library. Setiap target berkaitan 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 lainnya
  • srcs: file sumber yang akan dikompilasi untuk membuat artefak untuk target
  • deps: 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 halnya sistem build berbasis tugas, Anda dapat melakukan build menggunakan alat command line Bazel. Untuk membuat target MyBinary, jalankan bazel build :MyBinary. Setelah memasukkan perintah tersebut untuk pertama kalinya di repositori bersih, Bazel:

  1. Menguraikan setiap file BUILD di ruang kerja untuk membuat grafik dependensi di antara artefak.
  2. Menggunakan grafik untuk menentukan dependensi transitif MyBinary; yaitu, setiap target yang menjadi dependensi MyBinary dan setiap target yang menjadi target bergantung secara rekursif.
  3. Mem-build setiap dependensi tersebut secara berurutan. Bazel dimulai dengan mem-build setiap target yang tidak memiliki dependensi lain dan melacak dependensi yang masih perlu di-build untuk setiap target. Segera setelah semua dependensi target di-build, Bazel mulai mem-build target tersebut. Proses ini akan berlanjut sampai masing-masing dependensi transitif MyBinary dibuat.
  4. Mem-build MyBinary untuk menghasilkan biner akhir yang dapat dieksekusi yang ditautkan di semua dependensi yang di-build pada langkah 3.

Pada dasarnya, apa yang terjadi di sini mungkin tidak jauh berbeda dengan yang terjadi saat menggunakan sistem build berbasis tugas. Memang, hasil akhirnya adalah biner yang sama, dan proses untuk menghasilkannya melibatkan analisis serangkaian langkah untuk menemukan dependensi di antara keduanya, lalu menjalankan langkah-langkah tersebut secara berurutan. Namun, ada perbedaan mendasar. Yang pertama muncul di langkah 3: karena Bazel tahu bahwa setiap target hanya menghasilkan library Java, library tersebut tahu bahwa yang harus dilakukan hanyalah menjalankan compiler Java, bukan skrip yang ditentukan pengguna secara arbitrer, sehingga tahu bahwa aman untuk menjalankan langkah-langkah ini secara paralel. Hal ini dapat menghasilkan urutan peningkatan performa yang besar daripada target build satu per satu pada mesin multicore, dan hanya mungkin karena pendekatan berbasis artefak membiarkan sistem build bertanggung jawab atas strategi eksekusinya sendiri sehingga dapat memberikan jaminan yang lebih kuat tentang paralelisme.

Namun, manfaatnya melampaui paralelisme. Hal berikutnya yang akan diberikan oleh pendekatan ini adalah ketika developer mengetik bazel build :MyBinary untuk kedua kalinya tanpa membuat perubahan apa pun: Bazel akan keluar dalam waktu kurang dari satu detik dengan pesan yang menyatakan bahwa target sudah diupdate. Hal ini memungkinkan karena paradigma pemrograman fungsional yang telah kita bahas sebelumnya—Bazel tahu bahwa setiap target adalah hasil dari menjalankan compiler Java saja, dan mengetahui bahwa output dari compiler Java hanya bergantung pada inputnya, jadi selama input belum berubah, output dapat digunakan kembali. Dan analisis ini berfungsi di setiap tingkat; jika MyBinary.java berubah, Bazel tahu untuk mem-build ulang MyBinary tetapi menggunakan kembali mylib. Jika file sumber untuk //java/com/example/common berubah, Bazel tahu untuk mem-build ulang library tersebut, mylib, dan MyBinary, tetapi gunakan kembali //java/com/example/myproduct/otherlib. Karena tahu bahwa properti alat yang dijalankannya di setiap langkah, Bazel hanya dapat mem-build ulang kumpulan artefak minimum setiap kali menjamin tidak akan menghasilkan build yang sudah usang.

Membuat ulang proses build dalam hal artefak, bukan tugas, cukup sederhana, tetapi efektif. Dengan mengurangi fleksibilitas yang diekspos ke programmer, sistem build dapat mengetahui lebih lanjut hal yang sedang dilakukan di setiap langkah build. Alat ini dapat menggunakan pengetahuan ini untuk menjadikan 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 bagus lainnya

Sistem build berbasis artefak pada dasarnya menyelesaikan masalah terkait paralelisme dan penggunaan kembali yang melekat pada sistem build berbasis tugas. Namun, masih ada beberapa masalah yang muncul sebelumnya yang belum kami tangani. Bazel memiliki cara cerdas untuk menyelesaikan masalah ini, dan kita harus membahasnya sebelum melanjutkan.

Alat sebagai dependensi

Salah satu masalah yang kami hadapi sebelumnya adalah build yang bergantung pada alat yang diinstal di mesin kami, dan mereproduksi build di seluruh sistem bisa jadi sulit karena versi atau lokasi alat yang berbeda. Masalah ini menjadi lebih sulit saat project Anda menggunakan bahasa yang memerlukan alat berbeda berdasarkan platform yang digunakan untuk build atau kompilasi (seperti, Windows versus Linux), dan setiap platform tersebut memerlukan kumpulan 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 menjadi compiler terkenal. Setiap kali membuat Bazel, java_library 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 menyelesaikan bagian kedua masalah ini, yaitu independensi platform, dengan menetapkan konfigurasi build. Bukannya bergantung pada alat, bergantung pada jenis konfigurasi:

  • Konfigurasi host: alat build yang berjalan selama proses build
  • Konfigurasi target: mem-build biner yang pada akhirnya Anda minta

Memperluas sistem build

Bazel dilengkapi target untuk beberapa bahasa pemrograman populer secara langsung, tetapi engineer ingin selalu melakukan lebih banyak hal. Salah satu manfaat dari sistem berbasis tugas adalah fleksibilitasnya dalam mendukung segala jenis proses build, dan sebaiknya jangan menyerah dalam sistem build berbasis artefak. Untungnya, Bazel memungkinkan jenis target yang didukung untuk 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 oleh aturan tersebut. 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 level terendah dalam sistem build—tindakan dapat dilakukan apa pun selama tindakan tersebut hanya menggunakan input dan output yang dinyatakan, dan Bazel akan menangani tindakan penjadwalan dan menyimpan hasilnya dalam cache sebagaimana mestinya.

Sistem ini tidak dapat dibodohi mengingat tidak ada cara untuk menghentikan developer tindakan melakukan sesuatu seperti memperkenalkan proses nondeterministik sebagai bagian dari tindakan mereka. Namun, hal ini tidak terlalu sering terjadi dalam praktik dan mendorong kemungkinan untuk melakukan penyalahgunaan hingga ke tingkat tindakan akan sangat mengurangi peluang untuk error. Aturan yang mendukung banyak bahasa dan alat umum tersedia secara online, dan sebagian besar project tidak perlu menetapkan aturannya sendiri. Bahkan bagi mereka yang melakukannya, definisi aturan hanya perlu ditentukan di satu tempat terpusat dalam repositori, yang berarti sebagian besar engineer akan dapat menggunakan aturan tersebut tanpa harus mengkhawatirkan implementasinya.

Mengisolasi lingkungan

Sepertinya terdengar tindakan tersebut mengalami masalah yang sama dengan tugas di sistem lain. Apakah masih mungkin untuk menulis tindakan yang ditulis ke file yang sama dan akhirnya saling bertentangan? Sebenarnya, Bazel membuat konflik ini tidak mungkin dilakukan 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 menyertakan input yang telah dideklarasikan dan setiap output yang telah dihasilkannya. Hal ini diterapkan oleh sistem seperti LXC di Linux, teknologi yang sama di belakang Docker. Artinya, tindakan tidak mungkin bertentangan dengan satu hal lain karena tindakan ini 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 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 langsung mem-build-nya. 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, hal ini berisiko. File tersebut dapat berubah kapan saja, dan hal ini berpotensi mengharuskan sistem build untuk terus memeriksa apakah file tersebut masih baru. Jika file jarak jauh berubah tanpa perubahan yang sesuai dalam kode sumber ruang kerja, file tersebut juga dapat menyebabkan build yang tidak dapat direproduksi. Build mungkin bekerja satu hari dan gagal pada langkah berikutnya tanpa alasan yang jelas karena perubahan dependensi yang tidak diketahui. Terakhir, dependensi eksternal dapat menimbulkan risiko keamanan yang besar ketika dimiliki oleh pihak ketiga: jika penyerang dapat menyusup server pihak ketiga tersebut, ia dapat mengganti file dependensi dengan sesuatu yang didesain sendiri, yang berpotensi memberi mereka kontrol penuh atas lingkungan build dan outputnya.

Masalah dasarnya adalah kita ingin sistem build mengetahui file ini tanpa harus memeriksanya ke dalam kontrol sumber. Memperbarui dependensi harus menjadi pilihan yang sadar, tetapi pilihan tersebut harus dibuat satu kali di tempat pusat, bukan dikelola oleh masing-masing engineer atau secara otomatis oleh sistem. Hal ini karena meskipun dengan model “Live at Head”, kami masih menginginkan build yang bersifat deterministik, yang menyiratkan bahwa jika Anda melihat commit dari minggu lalu, Anda akan melihat dependensi tersebut sebagaimana adanya, 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 mencentang 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 aktual dependensi yang di-cache terhadap hash yang diharapkan yang ditentukan dalam manifes dan mendownload ulang file hanya jika hash tersebut berbeda.

Jika artefak yang didownload 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 data kapan dependensi diperbarui, dan dependensi eksternal tidak dapat berubah tanpa perubahan yang sesuai di sumber ruang kerja. Ini juga berarti bahwa, saat memeriksa versi lama kode sumber, build dijamin untuk menggunakan dependensi yang sama seperti yang digunakan pada saat versi tersebut diperiksa (atau akan gagal jika dependensi tersebut tidak lagi tersedia).

Tentu saja, masalah dapat terjadi jika server jarak jauh menjadi tidak tersedia atau mulai menayangkan data yang rusak—hal ini dapat menyebabkan semua build mulai gagal jika tidak ada salinan lain dari dependensi tersebut. Untuk menghindari masalah ini, sebaiknya buat, untuk setiap 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 untuk ketersediaan sistem build, meskipun hash yang dilaporkan menjamin keamanannya.