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 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 target
  • 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 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:

  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, 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.