Saat melihat halaman sebelumnya, ada satu tema yang berulang: mengelola kode Anda sendiri cukup mudah, tetapi mengelola dependensinya jauh lebih sulit. Ada berbagai jenis dependensi: terkadang ada dependensi pada tugas (seperti “dorong dokumentasi sebelum saya menandai rilis sebagai selesai”), dan terkadang ada dependensi pada artefak (seperti “saya perlu memiliki versi terbaru library computer vision untuk membuat kode saya”). Terkadang, Anda memiliki dependensi internal pada bagian lain dari codebase Anda, dan terkadang Anda memiliki dependensi eksternal pada kode atau data yang dimiliki oleh tim lain (baik di organisasi Anda maupun pihak ketiga). Namun, dalam kasus apa pun, gagasan “Saya membutuhkan itu sebelum saya bisa mendapatkan ini” adalah sesuatu yang berulang kali muncul dalam desain sistem build, dan mengelola dependensi mungkin merupakan tugas paling mendasar dari sistem build.
Menangani Modul dan Dependensi
Project yang menggunakan sistem build berbasis artefak seperti Bazel dibagi menjadi serangkaian
modul, dengan modul yang menyatakan dependensi satu sama lain melalui file BUILD
. Pengorganisasian modul dan dependensi yang tepat dapat memberikan pengaruh besar pada performa sistem build dan upaya yang diperlukan untuk pemeliharaannya.
Menggunakan Modul Terperinci dan Aturan 1:1:1
Pertanyaan pertama yang muncul saat menyusun build berbasis artefak adalah
menentukan seberapa banyak fungsi yang harus dicakup oleh setiap modul. Di Bazel,
modul direpresentasikan oleh target yang menentukan unit yang dapat dibangun seperti
java_library
atau go_binary
. Di satu sisi, seluruh project dapat
dimuat dalam satu modul dengan menempatkan satu file BUILD
di root dan
menggabungkan semua file sumber project tersebut secara rekursif. Di sisi lain, hampir setiap file sumber dapat dibuat menjadi modulnya sendiri, sehingga setiap file harus mencantumkan setiap file lain yang menjadi dependensinya dalam file BUILD
.
Sebagian besar project berada di antara dua ekstrem ini, dan pilihan melibatkan
pertukaran antara performa dan kemudahan pemeliharaan. Menggunakan satu modul untuk
seluruh project mungkin berarti Anda tidak perlu menyentuh file BUILD
kecuali
saat menambahkan dependensi eksternal, tetapi berarti sistem build harus
selalu membangun seluruh project sekaligus. Artinya, alat ini tidak akan dapat
memparalelkan atau mendistribusikan bagian build, dan tidak akan dapat menyimpan dalam cache bagian
yang telah dibuatnya. Satu modul per file adalah kebalikannya: sistem build
memiliki fleksibilitas maksimum dalam melakukan caching dan menjadwalkan langkah-langkah build, tetapi
engineer perlu berupaya lebih keras untuk memelihara daftar dependensi setiap kali
mereka mengubah file mana yang merujuk ke file mana.
Meskipun keakuratan yang tepat bervariasi menurut bahasa (dan sering kali bahkan dalam bahasa), Google cenderung lebih menyukai modul yang jauh lebih kecil daripada yang biasanya ditulis dalam sistem build berbasis tugas. Biner produksi umum di Google sering kali bergantung pada puluhan ribu target, dan bahkan tim berukuran sedang dapat memiliki beberapa ratus target dalam codebase-nya. Untuk bahasa seperti
Java yang memiliki konsep bawaan pengemasan yang kuat, setiap direktori biasanya
berisi satu paket, target, dan file BUILD
(Pants, sistem build lain
berbasis Bazel, menyebutnya aturan 1:1:1). Bahasa dengan konvensi pengemasan yang lebih lemah sering kali menentukan beberapa target per file BUILD
.
Manfaat target build yang lebih kecil mulai terlihat pada skala besar karena menghasilkan build terdistribusi yang lebih cepat dan lebih jarang perlu membangun ulang target.
Keuntungan ini menjadi lebih menarik setelah pengujian dilakukan, karena target yang lebih terperinci berarti sistem build dapat lebih cerdas dalam menjalankan hanya subset pengujian terbatas yang dapat terpengaruh oleh perubahan tertentu. Karena Google meyakini manfaat sistemik penggunaan target yang lebih kecil, kami telah membuat beberapa kemajuan dalam memitigasi kerugian 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 mana yang dapat bergantung padanya. Target pribadi
hanya dapat dirujuk dalam file BUILD
-nya sendiri. Target dapat memberikan visibilitas yang lebih luas ke target daftar file BUILD
yang ditentukan secara eksplisit, atau, dalam kasus visibilitas publik, ke setiap target di ruang kerja.
Seperti kebanyakan bahasa pemrograman, biasanya yang terbaik adalah meminimalkan visibilitas sebanyak mungkin. Umumnya, tim di Google akan memublikasikan target hanya jika target tersebut mewakili library yang banyak digunakan dan tersedia untuk tim mana pun di Google.
Tim yang mewajibkan orang lain untuk berkoordinasi dengan mereka sebelum menggunakan kode mereka akan mempertahankan daftar yang diizinkan dari target pelanggan sebagai visibilitas target mereka. Setiap
target penerapan internal tim akan dibatasi hanya pada direktori yang dimiliki oleh tim, dan sebagian besar file BUILD
hanya akan memiliki satu target yang tidak bersifat pribadi.
Mengelola dependensi
Modul harus dapat merujuk satu sama lain. Kelemahan memecah codebase menjadi modul yang terperinci adalah Anda perlu mengelola dependensi di antara modul tersebut (meskipun alat dapat membantu mengotomatiskan hal ini). Menyatakan dependensi ini biasanya menjadi sebagian besar konten dalam file BUILD
.
Dependensi internal
Dalam project besar yang dibagi menjadi modul terperinci, sebagian besar dependensi cenderung bersifat internal; yaitu, pada target lain yang ditentukan dan dibangun di repositori sumber yang sama. Dependensi internal berbeda dari dependensi eksternal karena dibangun dari sumber, bukan didownload sebagai artefak yang telah dibuat sebelumnya saat menjalankan build. Hal ini juga berarti tidak ada konsep “versi” untuk dependensi internal—target dan semua dependensi internalnya selalu dibuat pada commit/revisi yang sama di repositori. Salah satu masalah yang harus ditangani dengan cermat terkait dependensi internal adalah cara memperlakukan dependensi transitif (Gambar 1). Misalkan target A bergantung pada target B, yang bergantung pada target library umum C. Haruskah target A dapat menggunakan class yang ditentukan di target C?
Gambar 1. Dependensi transitif
Sejauh menyangkut alat pokok, tidak ada masalah dengan hal ini; B dan C akan ditautkan ke target A saat dibangun, sehingga simbol apa pun yang ditentukan dalam C diketahui oleh A. Bazel mengizinkan hal ini selama bertahun-tahun, tetapi seiring berkembangnya Google, kami mulai melihat masalah. Misalkan B difaktorkan ulang sehingga tidak lagi perlu bergantung pada C. Jika dependensi B pada C kemudian dihapus, A dan target lain yang menggunakan C melalui dependensi pada B akan rusak. Secara efektif, dependensi target menjadi bagian dari kontrak publiknya dan tidak pernah dapat diubah dengan aman. Hal ini berarti dependensi terakumulasi dari waktu ke waktu dan build di Google mulai melambat.
Google akhirnya menyelesaikan masalah ini dengan memperkenalkan “mode dependensi transitif ketat” di Bazel. Dalam mode ini, Bazel mendeteksi apakah target mencoba mereferensikan simbol tanpa bergantung langsung padanya dan, jika ya, akan gagal dengan error dan perintah shell yang dapat digunakan untuk menyisipkan dependensi secara otomatis. Meluncurkan perubahan ini di seluruh codebase Google dan memfaktorkan ulang setiap target build kami yang berjumlah jutaan untuk mencantumkan dependensinya secara eksplisit merupakan upaya selama bertahun-tahun, tetapi hal ini sangat bermanfaat. Build kami kini jauh lebih cepat karena target memiliki lebih sedikit dependensi yang tidak perlu, dan engineer dapat menghapus dependensi yang tidak mereka butuhkan tanpa khawatir merusak target yang bergantung padanya.
Seperti biasa, penerapan dependensi transitif yang ketat melibatkan pertukaran. Hal ini membuat
file build lebih panjang, karena library yang sering digunakan kini harus dicantumkan
secara eksplisit di banyak tempat, bukan ditarik secara kebetulan, dan engineer
perlu mengeluarkan lebih banyak upaya untuk menambahkan dependensi ke file BUILD
. Sejak saat itu, kami telah mengembangkan alat yang mengurangi pekerjaan ini dengan otomatis mendeteksi banyak dependensi yang tidak ada dan menambahkannya ke file BUILD
tanpa intervensi developer. Namun, bahkan tanpa alat tersebut, kami mendapati bahwa kompromi tersebut sangat
berharga seiring dengan penskalaan codebase: menambahkan dependensi secara eksplisit ke file BUILD
adalah biaya satu kali, tetapi menangani dependensi transitif implisit dapat menyebabkan
masalah berkelanjutan selama target build ada. Bazel menerapkan dependensi
transitif
yang ketat
pada kode Java secara default.
Dependensi eksternal
Jika dependensi tidak bersifat internal, maka harus bersifat eksternal. Dependensi eksternal adalah dependensi pada artefak yang dibangun dan disimpan di luar sistem build. Dependensi diimpor langsung dari repositori artefak (biasanya diakses melalui internet) dan digunakan sebagaimana adanya, bukan dibangun dari sumber. Salah satu perbedaan terbesar antara dependensi eksternal dan internal adalah bahwa dependensi eksternal memiliki versi, dan versi tersebut ada secara terpisah dari kode sumber project.
Pengelolaan dependensi otomatis versus manual
Sistem build dapat memungkinkan versi dependensi eksternal dikelola secara manual atau otomatis. Jika dikelola secara manual, file build akan mencantumkan secara eksplisit versi yang ingin didownload dari repositori artefak, sering kali menggunakan string versi semantik seperti 1.1.4
. Jika dikelola secara otomatis, file sumber akan menentukan rentang versi yang dapat diterima, dan sistem build akan selalu mendownload versi terbaru. Misalnya, Gradle memungkinkan versi dependensi dideklarasikan sebagai “1.+” untuk menentukan bahwa versi patch atau minor dependensi apa pun dapat diterima selama versi utamanya adalah 1.
Dependensi yang dikelola secara otomatis mungkin nyaman untuk project kecil, tetapi biasanya akan menimbulkan masalah pada project yang berukuran cukup besar atau yang dikerjakan oleh lebih dari satu engineer. Masalah dengan dependensi yang dikelola secara otomatis adalah Anda tidak dapat mengontrol kapan versinya diperbarui. Tidak ada cara untuk menjamin bahwa pihak eksternal tidak akan membuat update yang merusak (bahkan saat mereka mengklaim menggunakan versi semantik), sehingga build yang berfungsi pada suatu hari mungkin rusak pada hari berikutnya tanpa cara mudah untuk mendeteksi apa yang berubah atau mengembalikannya ke status berfungsi. Meskipun build tidak rusak, mungkin ada perubahan performa atau perilaku yang sulit dilacak.
Sebaliknya, karena dependensi yang dikelola secara manual memerlukan perubahan dalam kontrol sumber, dependensi tersebut dapat ditemukan dan di-roll back dengan mudah, dan Anda dapat meng-checkout versi repositori yang lebih lama untuk membangun dengan dependensi yang lebih lama. Bazel mengharuskan versi semua dependensi ditentukan secara manual. Bahkan pada skala yang sedang, overhead pengelolaan versi manual sangat berharga untuk stabilitas yang diberikannya.
Aturan Satu Versi
Versi library yang berbeda biasanya diwakili oleh artefak yang berbeda, jadi secara teori tidak ada alasan mengapa versi yang berbeda dari dependensi eksternal yang sama tidak dapat dideklarasikan dalam sistem build dengan nama yang berbeda. Dengan begitu, setiap target dapat memilih versi dependensi yang ingin digunakan. Hal ini menyebabkan banyak masalah dalam praktiknya, sehingga Google menerapkan Aturan Satu Versi yang ketat untuk semua dependensi pihak ketiga dalam codebase kami.
Masalah terbesar saat mengizinkan beberapa versi adalah masalah dependensi berlian. Misalkan target A bergantung pada target B dan pada library eksternal v1. Jika target B kemudian di-refactor untuk menambahkan dependensi pada v2 library eksternal yang sama, target A akan rusak karena kini bergantung secara implisit pada dua versi library yang sama. Sebenarnya, menambahkan dependensi baru dari target ke library pihak ketiga dengan beberapa versi tidak pernah aman, karena pengguna target tersebut mungkin sudah bergantung pada versi yang berbeda. Mengikuti Aturan Satu Versi membuat konflik ini tidak mungkin terjadi—jika target menambahkan dependensi pada library pihak ketiga, semua dependensi yang ada akan menggunakan versi yang sama, sehingga dapat berjalan bersama dengan baik.
Dependensi eksternal transitif
Menangani dependensi transitif dari dependensi eksternal bisa sangat sulit. Banyak repositori artefak seperti Maven Central, memungkinkan artefak menentukan dependensi pada versi tertentu dari artefak lain di repositori. Alat build seperti Maven atau Gradle sering kali mendownload setiap dependensi transitif secara rekursif secara default, yang berarti bahwa menambahkan satu dependensi dalam project Anda berpotensi menyebabkan puluhan artefak didownload secara total.
Hal ini sangat praktis: saat menambahkan dependensi pada library baru, akan sangat merepotkan jika harus melacak setiap dependensi transitif library tersebut dan menambahkannya secara manual. Namun, ada juga kerugian besar: karena library yang berbeda dapat bergantung pada versi yang berbeda dari library pihak ketiga yang sama, strategi ini pasti melanggar Aturan Satu Versi dan menyebabkan masalah dependensi berlian. Jika target Anda bergantung pada dua library eksternal yang menggunakan versi dependensi yang sama, tidak ada cara untuk mengetahui versi mana yang akan Anda dapatkan. Hal ini juga berarti bahwa mengupdate dependensi eksternal dapat menyebabkan kegagalan yang tampaknya tidak terkait di seluruh codebase jika versi baru mulai menarik versi yang bertentangan dari beberapa dependensinya.
Bazel tidak otomatis mendownload dependensi transitif. Sebelumnya, file WORKSPACE
digunakan untuk mencantumkan semua dependensi transitif yang diperlukan, sehingga menimbulkan banyak kesulitan saat mengelola dependensi eksternal. Bazel
telah menambahkan dukungan untuk pengelolaan dependensi eksternal transitif otomatis
dalam bentuk file MODULE.bazel
. Lihat ringkasan dependensi eksternal untuk mengetahui detail selengkapnya.
Sekali lagi, pilihan di sini adalah antara kemudahan dan skalabilitas. Project kecil mungkin lebih memilih untuk tidak perlu mengelola sendiri dependensi transitif dan mungkin dapat menggunakan dependensi transitif otomatis. Strategi ini menjadi kurang menarik seiring pertumbuhan organisasi dan codebase, serta konflik dan hasil yang tidak terduga menjadi semakin sering terjadi. Pada skala yang lebih besar, biaya pengelolaan dependensi secara manual jauh lebih rendah daripada biaya penanganan masalah yang disebabkan oleh pengelolaan dependensi otomatis.
Meng-cache hasil build menggunakan dependensi eksternal
Dependensi eksternal paling sering disediakan oleh pihak ketiga yang merilis library versi stabil, mungkin tanpa menyediakan kode sumber. Beberapa organisasi mungkin juga memilih untuk menyediakan beberapa kode mereka sendiri sebagai artefak, sehingga kode lain dapat bergantung pada kode tersebut sebagai dependensi pihak ketiga, bukan dependensi internal. Secara teoretis, hal ini dapat mempercepat build jika artefak lambat dibangun, tetapi cepat didownload.
Namun, hal ini juga menimbulkan banyak overhead dan kompleksitas: seseorang harus bertanggung jawab untuk membangun setiap artefak tersebut dan menguploadnya ke repositori artefak, dan klien harus memastikan bahwa mereka selalu menggunakan versi terbaru. Proses debug juga menjadi jauh lebih sulit karena berbagai bagian sistem akan dibangun dari berbagai titik dalam repositori, dan tidak ada lagi tampilan pohon sumber yang konsisten.
Cara yang lebih baik untuk mengatasi masalah artefak yang membutuhkan waktu lama untuk dibangun adalah dengan menggunakan sistem build yang mendukung penyimpanan cache jarak jauh, seperti yang dijelaskan sebelumnya. Sistem build seperti ini menyimpan artefak yang dihasilkan dari setiap build ke lokasi yang dibagikan di antara para engineer, jadi jika developer bergantung pada artefak yang baru saja di-build oleh orang lain, sistem build akan otomatis mendownloadnya daripada mem-build-nya. Hal ini memberikan semua manfaat performa dari bergantung langsung pada artefak sekaligus memastikan bahwa build sama konsistennya seolah-olah selalu dibuat dari sumber yang sama. Ini adalah strategi yang digunakan secara internal oleh Google, dan Bazel dapat dikonfigurasi untuk menggunakan cache jarak jauh.
Keamanan dan keandalan dependensi eksternal
Bergantung pada artefak dari sumber pihak ketiga pada dasarnya berisiko. Ada risiko ketersediaan jika sumber pihak ketiga (seperti repositori artefak) tidak berfungsi, karena seluruh build Anda mungkin terhenti jika tidak dapat mendownload dependensi eksternal. Ada juga risiko keamanan: jika sistem pihak ketiga disusupi oleh penyerang, penyerang dapat mengganti artefak yang dirujuk dengan desainnya sendiri, sehingga memungkinkan mereka menyuntikkan kode arbitrer ke dalam build Anda. Kedua masalah ini dapat diatasi dengan mencerminkan artefak yang Anda andalkan ke server yang Anda kontrol dan memblokir sistem build Anda agar tidak mengakses repositori artefak pihak ketiga seperti Maven Central. Sebagai gantinya, mirror ini memerlukan upaya dan resource untuk dipertahankan, sehingga pilihan untuk menggunakannya sering kali bergantung pada skala project. Masalah keamanan juga dapat dicegah sepenuhnya dengan sedikit overhead dengan mewajibkan hash setiap artefak pihak ketiga ditentukan di repositori sumber, sehingga menyebabkan build gagal jika artefak dimodifikasi. Alternatif
lain yang sepenuhnya menghindari masalah ini adalah dengan menyediakan dependensi
proyek Anda melalui vendor. Saat project menyediakan dependensinya, project akan memeriksanya ke dalam
kontrol sumber bersama dengan kode sumber project, baik sebagai sumber maupun sebagai
biner. Artinya, semua dependensi eksternal project
dikonversi menjadi dependensi internal. Google menggunakan pendekatan ini secara internal, dengan memeriksa setiap library pihak ketiga yang dirujuk di seluruh Google ke dalam direktori third_party
di root pohon sumber Google. Namun, hal ini hanya berfungsi di Google karena sistem kontrol sumber Google dibuat khusus untuk menangani monorepo yang sangat besar, sehingga vendoring mungkin bukan opsi untuk semua organisasi.