Sistem Build Berbasis Artefak

Laporkan masalah Lihat sumber Per malam · 7,3 · 7,2 · 7,1 · 7,0 · 6,5

Halaman ini membahas sistem build berbasis artefak dan filosofi di balik pembuatannya. 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. 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. 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 sekaligus tetap menjamin ketepatannya.

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, 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. 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 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 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 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 sekitar 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 {i>library<i} yang dapat digunakan oleh biner atau library lainnya. Setiap target memiliki:

  • name: cara target direferensikan di command line dan oleh target lain
  • srcs: file sumber yang akan dikompilasi guna membuat artefak untuk target
  • deps: target lain yang harus dibangun sebelum target ini dan ditautkan ke dalam ini

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:

  1. Mengurai setiap file BUILD di ruang kerja untuk membuat grafik dependensi di antara artefak.
  2. Menggunakan grafik untuk menentukan dependensi transitif MyBinary; sehingga adalah, setiap target yang menjadi dependensi MyBinary dan setiap target yang target bergantung pada, secara rekursif.
  3. 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.
  4. Mem-build MyBinary untuk menghasilkan biner final yang dapat dieksekusi yang tertaut di semua dependensi yang dibuat 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 pembuatannya 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 {i>library<i} Java, maka 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 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. 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. Dan analisis ini bekerja di setiap level; jika MyBinary.java berubah, Bazel akan tahu untuk membangun 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 tahu tentang properti alat yang dijalankannya di setiap langkah, hanya dapat membangun kembali kumpulan minimum artefak setiap kali menjamin bahwa proses ini tidak akan menghasilkan build yang usang.

Memformat ulang proses build dalam hal artefak, bukan tugas, adalah hal yang halus tetapi efektif. Dengan mengurangi fleksibilitas yang diekspos ke programmer, build sistem dapat mengetahui lebih banyak tentang apa yang sedang dilakukan pada setiap langkah pembangunan. 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 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 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 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 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 menyelesaikan bagian kedua masalah, independensi platform, dengan menetapkan konfigurasi build. Bukan target bergantung langsung pada alat mereka, target tersebut bergantung pada jenis konfigurasi:

  • Konfigurasi host: membuat alat 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 untuk diperluas oleh 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. 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, hal ini tidak sering terjadi dalam praktiknya, dan mendorong kemungkinan penyalahgunaan hingga ke tingkat tindakan akan sangat mengurangi kemungkinan terjadinya error. Aturan yang mendukung banyak bahasa dan alat umum banyak tersedia secara {i>online<i}, dan sebagian besar proyek tidak perlu menentukan sendiri aturan. 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 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 konflik ini tidak mungkin terjadi dengan menggunakan sandboxing. 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, 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 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 sangat 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 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 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 memeriksanya 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. Hash adalah cara ringkas untuk merepresentasikan file secara unik tanpa memeriksa 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. 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. 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.