Saat melihat halaman sebelumnya, satu tema berulang: mengelola kode Anda sendiri cukup mudah, tetapi mengelola dependensinya akan lebih sulit. Ada berbagai macam ketergantungan: terkadang ada dependensi pada tugas (seperti "{i>push<i} dokumentasi sebelum saya menandai rilis sebagai lengkap”), dan terkadang ada ketergantungan pada suatu artefak (seperti “Saya perlu memiliki library computer vision versi terbaru untuk membuat kode saya”). Terkadang, Anda memiliki dependensi internal pada bagian lain codebase Anda, dan terkadang Anda memiliki ketergantungan eksternal pada kode atau data yang dimiliki oleh tim lain (baik di organisasi Anda maupun pihak ketiga). Tapi bagaimanapun juga, gagasan tentang “Saya memerlukan itu sebelum saya dapat memiliki ini” adalah sesuatu yang berulang kali dalam sistem build, dan mengelola dependensi mungkin yang paling dasar sistem build.
Menangani Modul dan Dependensi
Project yang menggunakan sistem build berbasis artefak seperti Bazel dipecah menjadi satu set
modul, dengan modul yang mengekspresikan dependensi satu sama lain melalui BUILD
. Pengaturan yang tepat dari modul dan dependensi ini
dapat memiliki dampak yang besar
berpengaruh pada kinerja sistem pembangunan dan berapa banyak pekerjaan yang diperlukan untuk
pertahankan.
Menggunakan Modul Berbutir Halus dan Aturan 1:1:1
Pertanyaan pertama yang muncul saat menyusun build berbasis artefak adalah
memutuskan seberapa banyak fungsi
yang harus disertakan dalam modul individu. Di Bazel,
modul direpresentasikan oleh target yang menentukan unit yang dapat dibangun seperti
java_library
atau go_binary
. Pada satu ekstrem, seluruh proyek bisa
yang ada dalam satu modul dengan meletakkan satu file BUILD
di root dan
secara rekursif menggabungkan semua
file sumber proyek itu. Di sisi lain
hampir setiap file sumber dapat dibuat
ke dalam modul sendiri, secara efektif
mengharuskan setiap file dicantumkan dalam file BUILD
untuk setiap file lain yang menjadi dependensinya.
Sebagian besar proyek berada di antara titik ekstrem ini, dan pemilihannya melibatkan
kompromi antara performa
dan kemudahan pemeliharaan. Menggunakan satu modul untuk
keseluruhan project mungkin berarti Anda tidak perlu menyentuh file BUILD
kecuali
saat menambahkan dependensi eksternal, tetapi itu berarti bahwa sistem build harus
selalu membangun
seluruh proyek sekaligus. Artinya, ia tidak akan dapat
memparalelkan atau mendistribusikan bagian-bagian build, serta tidak dapat meng-cache bagian
bahwa layanan tersebut sudah dibuat. Satu modul-per-file adalah kebalikannya: sistem build
memiliki fleksibilitas maksimum dalam langkah-langkah caching dan penjadwalan build, tetapi
insinyur perlu berupaya lebih keras dalam
mengelola daftar dependensi setiap kali
mereka mengubah {i>file<i}
yang merujuk ke mana.
Meskipun perincian yang tepat bervariasi menurut bahasa (dan bahkan sering dalam
bahasa), Google cenderung mendukung modul yang jauh lebih kecil daripada
yang biasanya ditulis dalam sistem
build berbasis tugas. Biner produksi yang umum di
Google sering kali bergantung pada puluhan ribu target, dan bahkan target berukuran sedang
dapat memiliki beberapa ratus target dalam codebase-nya. Untuk bahasa seperti
Java yang memiliki konsep pengemasan bawaan yang kuat, setiap direktori biasanya
berisi satu paket, target, dan file BUILD
(Celana, sistem build lain
menurut Bazel, menyebutnya aturan 1:1:1). Bahasa dengan paket yang lebih lemah
sering kali menentukan beberapa target per file BUILD
.
Manfaat target versi yang lebih kecil
benar-benar mulai terlihat dalam skala besar karena
menghasilkan build yang terdistribusi lebih cepat dan mengurangi kebutuhan untuk membangun ulang target.
Kelebihannya menjadi lebih menarik setelah
digambarkan, karena pengujian
target yang lebih detail berarti sistem build bisa jauh lebih pintar dalam
yang hanya menjalankan subset terbatas dari pengujian yang dapat dipengaruhi oleh
berubah. Karena Google percaya pada manfaat sistemik dari penggunaan yang lebih kecil
target, kami telah mengambil beberapa langkah dalam mengurangi sisi negatif dengan berinvestasi dalam
alat untuk mengelola file BUILD
secara otomatis agar tidak membebani developer.
Beberapa alat ini, seperti buildifier
dan buildozer
, tersedia dengan
Bazel di
Direktori buildtools
.
Meminimalkan Visibilitas Modul
Bazel dan sistem build lainnya memungkinkan setiap target menentukan visibilitas —
properti yang menentukan target lain yang mungkin bergantung padanya. Target pribadi
hanya dapat direferensikan dalam file BUILD
-nya sendiri. Target dapat memberikan
visibilitas ke target daftar file BUILD
yang ditentukan secara eksplisit, atau, di
visibilitas publik, ke setiap target di ruang kerja.
Seperti kebanyakan bahasa pemrograman, sebaiknya
meminimalkan visibilitas sebagai
sebanyak mungkin. Umumnya, tim di Google akan membuat target bersifat publik hanya jika
target tersebut mewakili perpustakaan yang banyak digunakan,
yang tersedia untuk tim mana pun di Google.
Tim yang mengharuskan orang lain berkoordinasi dengan mereka sebelum menggunakan kode mereka akan
mempertahankan daftar target pelanggan yang diizinkan sebagai visibilitas target mereka. Masing-masing
target implementasi internal tim akan dibatasi hanya untuk direktori
dimiliki oleh tim, dan sebagian besar file BUILD
hanya akan memiliki satu target yang tidak
pribadi.
Mengelola dependensi
Modul harus dapat saling merujuk satu sama lain. Kelemahan dari merusak
menjadi modul terperinci adalah Anda perlu mengelola dependensi
di antara modul tersebut (meskipun alat dapat membantu mengotomatiskannya). Dengan menyatakan
dependensi biasanya menjadi bagian terbesar dari konten dalam file BUILD
.
Dependensi internal
Dalam sebuah proyek besar yang dibagi menjadi modul-modul halus, sebagian besar dependensi mungkin bersifat internal; yaitu, pada target lain yang ditentukan dan dibuat dengan ke repositori sumber. Dependensi internal berbeda dari dependensi eksternal di bahwa build tersebut dibuat dari sumber, bukan didownload sebagai artefak bawaan saat menjalankan build. Ini juga berarti bahwa tidak ada istilah “versi” untuk dependensi internal—target dan semua dependensi internalnya selalu dibangun pada commit/revisi yang sama dalam repositori. Satu masalah yang harus diperhatikan ditangani dengan hati-hati sehubungan dengan dependensi internal, adalah bagaimana memperlakukan dependensi transitif (Gambar 1). Misalkan target A bergantung pada target B, yang bergantung pada target library umum C. Harus menargetkan A agar dapat menggunakan class yang didefinisikan dalam target C?
Gambar 1. Dependensi transitif
Selama alat yang mendasarinya, tidak ada masalah dengan hal ini; keduanya B dan C akan ditautkan ke target A ketika dibangun, jadi setiap simbol yang ditentukan dalam C dikenal oleh A. Bazel mengizinkannya selama bertahun-tahun, tetapi seiring dengan berkembangnya Google, kami mulai menemui masalah. Misalkan B telah difaktorkan ulang sedemikian rupa sehingga tidak lagi yang diperlukan untuk bergantung pada C. Jika ketergantungan B pada C kemudian dihapus, A dan target yang menggunakan C melalui dependensi pada B akan rusak. Secara efektif, dependensi menjadi bagian dari kontrak publiknya dan tidak pernah bisa ubah. Ini berarti ketergantungan yang diakumulasi seiring waktu dan dibangun di Google mulai melambat.
Google akhirnya menyelesaikan masalah ini dengan memperkenalkan model mode dependensi” di Bazel. Dalam mode ini, Bazel mendeteksi apakah target mencoba mereferensikan simbol tanpa bergantung padanya secara langsung dan, jika demikian, akan gagal dengan dan perintah {i>shell <i}yang dapat digunakan untuk memasukkan dependensi. Meluncurkan perubahan ini di seluruh codebase Google dan memfaktorkan ulang setiap jutaan target build untuk mencantumkan secara eksplisit dependensi adalah upaya yang berlangsung selama bertahun-tahun, tapi hal itu sangat bermanfaat. Build kami kini jauh lebih cepat mengingat target memiliki lebih sedikit dependensi yang tidak perlu, dan insinyur diberdayakan untuk menghilangkan ketergantungan yang tidak mereka perlukan tanpa khawatir tentang melanggar target yang bergantung pada mereka.
Seperti biasa, menegakkan dependensi transitif yang ketat melibatkan kompromi. Itu membuat
file build menjadi lebih panjang, karena library yang sering digunakan kini harus dicantumkan
secara eksplisit di banyak tempat alih-alih menarik secara tidak sengaja, dan para insinyur
diperlukan lebih banyak upaya untuk menambahkan dependensi ke file BUILD
. Sejak saat itu
mengembangkan alat yang mengurangi toil ini dengan secara otomatis mendeteksi banyak
dependensi dan menambahkannya ke file BUILD
tanpa developer apa pun
intervensi. Namun, meski tanpa alat seperti itu, komprominya sangat baik
sepadan dengan skala codebase: secara eksplisit menambahkan dependensi ke file BUILD
adalah biaya satu kali, tetapi berurusan dengan
dependensi transitif implisit dapat menyebabkan
masalah yang sedang berlangsung selama target build ada. Roti Bazel
menerapkan dependensi transitif yang ketat
kode Java secara default.
Dependensi eksternal
Jika tidak bersifat internal, dependensi harus bersifat eksternal. Dependensi eksternal adalah artefak yang dibangun dan disimpan di luar sistem build. Tujuan dependensi diimpor langsung dari repositori artefak (biasanya diakses melalui internet) dan digunakan apa adanya dan bukan dibuat dari sumber. Salah satu perbedaan terbesar antara dependensi eksternal dan internal adalah dependensi eksternal memiliki versi, dan versi tersebut berada secara terpisah dari kode sumber proyek.
Pengelolaan dependensi otomatis versus manual
Sistem build dapat mengizinkan pengelolaan versi dependensi eksternal
baik secara manual
maupun otomatis. Jika dikelola secara manual, buildfile
secara eksplisit mencantumkan versi yang
ingin diunduh dari repositori artefak,
sering kali menggunakan string versi semantik seperti
sebagai 1.1.4
. Jika dikelola secara otomatis, file sumber menentukan rentang
yang dapat diterima, dan sistem pembangunan
selalu mengunduh versi terbaru. Sebagai
misalnya, Gradle mengizinkan versi dependensi dideklarasikan sebagai “1.+” untuk menentukan
bahwa versi minor atau patch dari
dependensi dapat diterima selama
versi utamanya adalah 1.
Dependensi yang dikelola secara otomatis mungkin nyaman untuk proyek kecil, tetapi Mereka biasanya menjadi resep bencana pada proyek dengan ukuran yang besar atau sedang dikerjakan oleh lebih dari satu insinyur. Masalahnya dengan sistem dependensi terkelola adalah Anda tidak memiliki kendali atas kapan versi diperbarui. Tidak ada cara untuk menjamin bahwa pihak eksternal tidak akan melakukan pelanggaran update terbaru (bahkan saat mereka mengklaim menggunakan pembuatan versi semantik), jadi build bekerja di suatu hari mungkin rusak di hari berikutnya tanpa cara mudah untuk mendeteksi apa yang berubah atau untuk melakukan roll back ke keadaan kerja. Bahkan jika bangunannya tidak rusak, dapat berupa perubahan perilaku atau kinerja halus yang mustahil untuk dilacak.
Sebaliknya, karena dependensi yang dikelola secara manual memerlukan perubahan pada tetap mudah ditemukan dan di-roll back, serta memungkinkan periksa repositori versi lama untuk membangun dengan dependensi yang lebih lama. Bazel mengharuskan versi semua dependensi ditentukan secara manual. Bahkan skala sedang, overhead pengelolaan versi manual sepadan dengan harganya stabilitas yang diberikannya.
Aturan Satu Versi
Versi {i>library<i} yang berbeda biasanya diwakili oleh artefak yang berbeda, Jadi secara teori tidak ada alasan bahwa versi berbeda dari sumber daya dependensi tidak dapat dideklarasikan keduanya dalam sistem build dengan nama yang berbeda. Dengan begitu, setiap target dapat memilih versi dependensi mana yang ingin gunakan. Hal ini menyebabkan banyak masalah dalam praktik, jadi Google memberlakukan Aturan Satu Versi untuk semua dependensi pihak ketiga di codebase kami.
Masalah terbesar dalam mengizinkan beberapa versi adalah dependensi diamond masalah performa. Misalkan target A bergantung pada target B dan pada v1 eksternal library. Jika target B difaktorkan ulang untuk menambahkan dependensi pada v2 library eksternal, target A akan rusak karena sekarang bergantung secara implisit pada dua versi berbeda dari pustaka yang sama. Sebenarnya, tidak pernah aman untuk menambahkan dependensi baru dari target ke library pihak ketiga mana pun dengan beberapa versi, karena salah satu pengguna target tersebut bisa bergantung pada . Mengikuti Aturan Satu Versi membuat konflik ini tidak mungkin terjadi—jika target menambahkan dependensi pada library pihak ketiga, dependensi apa pun sudah berada di versi yang sama, sehingga mereka dapat hidup berdampingan.
Dependensi eksternal transitif
Menangani dependensi transitif dari dependensi eksternal dapat hal ini sangat sulit. Banyak repositori artefak seperti Maven Central, artefak untuk menentukan dependensi pada versi tertentu dari artefak lain dalam repositori. Alat build seperti Maven atau Gradle sering mendownload masing-masing secara rekursif dependensi transitif secara default, artinya menambahkan satu dependensi proyek Anda berpotensi menyebabkan lusinan artefak diunduh di total.
Ini sangat nyaman: ketika menambahkan dependensi pada {i>library<i} baru, akan kesulitan besar untuk melacak setiap dependensi transitif {i>library<i} dan menambahkan semuanya secara manual. Tapi ada juga kerugian besar: karena perbedaan bisa bergantung pada versi yang berbeda dari pustaka pihak ketiga yang sama, maka strategi selalu melanggar Aturan Satu Versi dan menyebabkan dependensi. Jika target Anda bergantung pada dua library eksternal yang menggunakan versi berbeda dari dependensi yang sama, tidak ada yang dapat dapatkan. Ini juga berarti bahwa memperbarui dependensi eksternal dapat menyebabkan kegagalan yang tidak terkait di seluruh codebase jika versi baru mulai digunakan versi yang bertentangan dari beberapa dependensinya.
Karena alasan ini, Bazel tidak secara otomatis mengunduh dependensi transitif.
Dan, sayangnya, tidak ada solusi alternatif — alternatif Bazel adalah mengharuskan
yang mencantumkan setiap resource eksternal repositori
dependensi dan versi eksplisit yang digunakan
untuk dependensi itu di seluruh
repositori resource. Untungnya, Bazel menyediakan
alat yang mampu secara otomatis
membuat file seperti itu yang berisi dependensi transitif dari satu set Maven
artefak. Alat ini dapat dijalankan sekali untuk membuat file WORKSPACE
awal
untuk sebuah proyek, dan file tersebut kemudian
dapat diperbarui secara manual untuk menyesuaikan
dari setiap dependensi.
Sekali lagi, pilihan di sini adalah antara kenyamanan dan skalabilitas. Kecil proyek mungkin lebih memilih untuk tidak perlu mengkhawatirkan pengelolaan dependensi transitif sendiri dan mungkin bisa menggunakan penggunaan transitif otomatis dependensi. Strategi ini menjadi kurang menarik karena organisasi ini dan codebase berkembang, konflik dan hasil yang tak terduga menjadi semakin sering dilakukan. Pada skala yang lebih besar, biaya pengelolaan dependensi secara manual jauh lebih murah daripada biaya untuk mengatasi masalah yang disebabkan oleh dependensi otomatis otomatisasi pengelolaan biaya.
Menyimpan cache hasil build menggunakan dependensi eksternal
Ketergantungan eksternal paling sering disediakan oleh pihak ketiga yang mengeluarkan versi stabil dari library, mungkin tanpa menyediakan kode sumber. Agak besar organisasi juga dapat memilih untuk menyediakan beberapa kode mereka sendiri sebagai artefak, sehingga memungkinkan potongan kode lain bergantung padanya sebagai daripada dependensi internal. Hal ini secara teoritis dapat mempercepat build jika artefak lambat dibangun tetapi cepat diunduh.
Namun, hal ini juga menimbulkan banyak {i>overhead<i} dan kompleksitas: seseorang harus bertanggung jawab untuk membangun setiap artefak itu dan menguploadnya ke artefak artefak, dan klien perlu memastikan bahwa mereka tetap diperbarui dengan ke versi terbaru. {i>Debugging<i} juga menjadi jauh lebih sulit karena perbedaan bagian-bagian dari sistem akan dibangun dari titik-titik yang berbeda dalam repositori, dan tidak ada lagi tampilan hierarki sumber yang konsisten.
Cara yang lebih baik untuk memecahkan masalah artefak yang membutuhkan waktu lama untuk dibangun adalah dengan menggunakan sistem pembangunan yang mendukung {i> cache<i} jarak jauh, seperti dijelaskan sebelumnya. Sungguh sistem build menyimpan artefak yang dihasilkan dari setiap build ke lokasi yang dibagikan kepada seluruh engineer, jadi jika developer bergantung pada artefak yang baru-baru ini dibuat oleh orang lain, sistem build akan otomatis mendownload alih-alih membangunnya. Hal ini memberikan semua manfaat kinerja tergantung langsung pada artefak sambil tetap memastikan bahwa build konsisten seolah-olah mereka selalu dibuat dari sumber yang sama. Ini adalah yang digunakan secara internal oleh Google, dan Bazel dapat dikonfigurasi untuk menggunakan di cache oleh pengguna.
Keamanan dan keandalan dependensi eksternal
Bergantung pada artefak dari sumber pihak ketiga, berisiko secara inheren. Terdapat
risiko ketersediaan jika sumber pihak ketiga (seperti repositori artefak)
karena seluruh build mungkin akan berhenti jika tidak dapat didownload
dependensi eksternal. Terdapat juga risiko keamanan: jika sistem pihak ketiga
disusupi oleh penyerang, maka penyerang
dapat mengganti perangkat yang direferensikan
artefak dengan salah satu desainnya sendiri, yang memungkinkan mereka memasukkan kode arbitrer
ke dalam build Anda. Kedua masalah tersebut dapat dimitigasi dengan
mencerminkan artefak apa pun yang
bergantung pada server yang Anda kontrol dan memblokir akses sistem build
repositori artefak pihak ketiga seperti Maven Central. Konsekuensinya adalah
cermin ini membutuhkan usaha dan
sumber daya untuk dipelihara, jadi pilihan apakah akan
menggunakannya sering tergantung
pada skala proyek. Masalah keamanan juga dapat
sepenuhnya dicegah dengan {i>overhead<i}
yang membutuhkan {i>hash <i}dari setiap
artefak pihak ketiga ditentukan dalam repositori sumber, sehingga menyebabkan
gagal jika artefak telah dirusak. Alternatif lain yang sepenuhnya
langkah sampingan masalahnya adalah memasok dependensi proyek Anda. Ketika sebuah proyek
dependensinya, vendor ini memeriksanya ke dalam kontrol sumber bersama
kode sumber project Anda, baik sebagai sumber atau biner. Hal ini secara efektif berarti
bahwa semua dependensi eksternal proyek dikonversi menjadi
dependensi. Google menggunakan pendekatan ini secara internal, memeriksa setiap pihak ketiga
library yang dirujuk di seluruh Google ke dalam direktori third_party
di root
pohon sumber Google. Namun, fitur ini hanya berfungsi di Google karena
yang dibuat khusus untuk menangani monorepo yang sangat besar, jadi
vendor mungkin tidak menjadi
pilihan untuk semua organisasi.