Önceki sayfalara göz attığımızda bir temanın tekrar tekrar karşımıza çıktığını görüyoruz: Kendi kodunuzu yönetmek oldukça kolaydır ancak bağımlılıklarını yönetmek çok daha zordur. Her türlü bağımlılık vardır: Bazen bir göreve (ör. "Yayınlamayı tamamlandı olarak işaretlemeden önce dokümanları gönder") bazen de bir yapıta (ör. "Kodumu oluşturmak için bilgisayarla görme kitaplığının en son sürümüne sahip olmam gerekiyor") bağımlılık vardır. Bazen kod tabanınızın başka bir bölümüne dahili bağımlılıklarınız, bazen de kuruluşunuzdaki veya üçüncü taraf bir ekipten olan kod ya da verilere harici bağımlılıklarınız vardır. Ancak her durumda, "Bunu alabilmem için önce ona ihtiyacım var" fikri, derleme sistemlerinin tasarımında tekrar tekrar ortaya çıkan bir durumdur ve bağımlılıkları yönetmek, derleme sisteminin belki de en temel işidir.
Modüller ve Bağımlılıklarla İlgili Sorunları Çözme
Bazel gibi artefakt tabanlı derleme sistemlerini kullanan projeler bir dizi modüle ayrılır. Modüller, BUILD
dosyaları aracılığıyla birbirlerine olan bağımlılıklarını ifade eder. Bu modüllerin ve bağımlılıkların uygun şekilde düzenlenmesi, hem derleme sisteminin performansı hem de bakımının ne kadar iş gerektirdiği konusunda büyük bir etkiye sahip olabilir.
Ayrıntılı Modülleri ve 1:1:1 Kuralını Kullanma
Yapılandırma yapıtı tabanlı bir derleme oluştururken akla gelen ilk soru, tek bir modülün ne kadar işlevsellik içermesi gerektiğine karar vermektir. Bazel'de modül, java_library
veya go_binary
gibi oluşturulabilir bir birimi belirten bir hedefle temsil edilir. Bir uç noktada, köke bir BUILD
dosyası yerleştirilerek ve bu projenin tüm kaynak dosyaları yinelemeli olarak bir araya getirilerek projenin tamamı tek bir modülde yer alabilir. Diğer uçta ise neredeyse her kaynak dosya kendi modülüne dönüştürülebilir. Bu durumda, her dosyanın bağlı olduğu diğer tüm dosyaları bir BUILD
dosyasında listelemesi gerekir.
Çoğu proje bu uç noktalar arasında bir yerde yer alır ve seçim, performans ile sürdürülebilirlik arasında bir denge kurmayı gerektirir. Projenin tamamı için tek bir modül kullanmak, harici bağımlılık ekleme dışında BUILD
dosyasına hiç dokunmamanız gerektiği anlamına gelebilir ancak bu durumda derleme sisteminin her zaman projenin tamamını tek seferde oluşturması gerekir. Bu nedenle, derlemenin bölümlerini paralelleştiremez veya dağıtamaz ya da önceden oluşturduğu bölümleri önbelleğe alamaz. Dosya başına bir modül ise bunun tam tersidir: Derleme sistemi, derlemenin adımlarını önbelleğe alma ve planlama konusunda maksimum esnekliğe sahiptir ancak mühendislerin, hangi dosyaların hangilerine referans verdiğini her değiştirdiklerinde bağımlılık listelerini korumak için daha fazla çaba harcaması gerekir.
Kesin ayrıntı düzeyi dile göre (ve genellikle dil içinde bile) değişse de Google, genellikle görev tabanlı bir derleme sisteminde yazılacak olanlardan önemli ölçüde daha küçük modülleri tercih eder. Google'daki tipik bir üretim ikilisi genellikle on binlerce hedefe bağlıdır ve orta büyüklükteki bir ekip bile kod tabanında birkaç yüz hedefe sahip olabilir. Java gibi paketleme kavramı yerleşik olarak güçlü olan dillerde her dizin genellikle tek bir paket, hedef ve BUILD
dosyası içerir (Pants, Bazel tabanlı başka bir derleme sistemi, buna 1:1:1 kuralı adını verir). Daha zayıf paketleme kurallarına sahip dillerde genellikle BUILD
dosyası başına birden fazla hedef tanımlanır.
Daha küçük derleme hedeflerinin avantajları, dağıtılmış derlemelerin daha hızlı olması ve hedeflerin daha az sıklıkta yeniden oluşturulması gerektiği için büyük ölçekte kendini göstermeye başlar.
Testler devreye girdikten sonra avantajlar daha da belirginleşir. Daha ayrıntılı hedefler, derleme sisteminin yalnızca belirli bir değişiklikten etkilenebilecek sınırlı bir test alt kümesini çalıştırma konusunda çok daha akıllı olabileceği anlamına gelir. Google, daha küçük hedeflerin sistemik faydalarına inandığı için geliştiricilere yük olmamak adına BUILD
dosyalarını otomatik olarak yöneten araçlara yatırım yaparak bu durumun olumsuz etkilerini azaltma konusunda bazı adımlar attı.
buildifier
ve buildozer
gibi bu araçlardan bazıları, buildtools
dizininde Bazel ile kullanılabilir.
Modül görünürlüğünü en aza indirme
Bazel ve diğer derleme sistemleri, her hedefin bir görünürlük belirtmesine olanak tanır. Görünürlük, hangi diğer hedeflerin kendisine bağlı olabileceğini belirleyen bir özelliktir. Özel bir hedef yalnızca kendi BUILD
dosyası içinde referans verilebilir. Bir hedef, açıkça tanımlanmış bir BUILD
dosya listesinin hedeflerine daha geniş bir görünürlük sağlayabilir veya herkese açık görünürlük durumunda çalışma alanındaki her hedefe daha geniş bir görünürlük sağlayabilir.
Çoğu programlama dilinde olduğu gibi, görünürlüğü mümkün olduğunca en aza indirmek genellikle en iyisidir. Genel olarak Google'daki ekipler, hedefleri yalnızca Google'daki herhangi bir ekibin kullanabileceği, yaygın olarak kullanılan kitaplıkları temsil ediyorsa herkese açık hale getirir.
Kodlarını kullanmadan önce diğerlerinin kendileriyle koordinasyon kurmasını gerektiren ekipler, hedef kitlelerinin görünürlüğü olarak müşteri hedeflerinin izin verilenler listesini tutar. Her ekibin dahili uygulama hedefleri yalnızca ekibe ait dizinlerle sınırlandırılır ve çoğu BUILD
dosyasının yalnızca bir hedefi olur. Bu hedef özel değildir.
Bağımlılıkları Yönetme
Modüllerin birbirine başvurabilmesi gerekir. Kod tabanını ayrıntılı modüllere ayırmanın dezavantajı, bu modüller arasındaki bağımlılıkları yönetmeniz gerekmesidir (ancak araçlar bu işlemi otomatikleştirmenize yardımcı olabilir). Bu bağımlılıkları ifade etmek genellikle bir BUILD
dosyasındaki içeriğin büyük bir kısmını oluşturur.
İç bağımlılıklar
İnce ayrıntılı modüllere ayrılmış büyük bir projede, bağımlılıkların çoğu muhtemelen dahili olacaktır. Yani aynı kaynak deposunda tanımlanmış ve oluşturulmuş başka bir hedefe bağlı olacaktır. İç bağımlılıklar, derleme çalıştırılırken önceden oluşturulmuş bir yapıt olarak indirilmek yerine kaynaktan oluşturuldukları için dış bağımlılıklardan farklıdır. Bu aynı zamanda dahili bağımlılıklar için "sürüm" kavramının olmadığı anlamına da gelir. Bir hedef ve tüm dahili bağımlılıkları her zaman depodaki aynı commit/revizyon ile oluşturulur. İç bağımlılıklarla ilgili olarak dikkatli bir şekilde ele alınması gereken bir sorun, geçişli bağımlılıkların nasıl ele alınacağıdır (Şekil 1). A hedefinin B hedefine, B hedefinin ise ortak bir kitaplık hedefi olan C hedefine bağlı olduğunu varsayalım. Hedef A, hedef C'de tanımlanan sınıfları kullanabilir mi?
Şekil 1. Geçişli bağımlılıklar
Temel araçlar açısından bu konuda sorun yoktur. Hem B hem de C, oluşturulduğunda A hedefiyle bağlantılı olacağından C'de tanımlanan tüm semboller A tarafından bilinir. Bazel, bu duruma uzun yıllar boyunca izin verdi ancak Google büyüdükçe sorunlar görmeye başladık. B'nin, artık C'ye bağlı olması gerekmeyecek şekilde yeniden düzenlendiğini varsayalım. B'nin C'ye olan bağımlılığı kaldırılırsa A ve B'ye bağımlı olarak C'yi kullanan diğer tüm hedefler bozulur. Hedeflerin bağımlılıkları, herkese açık sözleşmelerinin bir parçası haline geldi ve hiçbir zaman güvenli bir şekilde değiştirilemedi. Bu durum, zaman içinde bağımlılıkların birikmesine ve Google'daki derlemelerin yavaşlamasına neden oldu.
Google, Bazel'de "katı geçişli bağımlılık modu"nu tanıtarak bu sorunu çözdü. Bu modda Bazel, bir hedefin doğrudan bağlı olmadan bir sembole başvurmaya çalışıp çalışmadığını algılar. Eğer böyle bir durum varsa hata ve bağımlılığı otomatik olarak eklemek için kullanılabilecek bir kabuk komutuyla başarısız olur. Bu değişikliği Google'ın tüm kod tabanında kullanıma sunmak ve milyonlarca derleme hedefimizin her birini, bağımlılıklarını açıkça listeleyecek şekilde yeniden düzenlemek yıllar süren bir çaba gerektirdi ancak bu çaba karşılığını verdi. Hedeflerde daha az gereksiz bağımlılık olduğundan ve mühendisler, kendilerine bağlı hedefleri bozma endişesi olmadan ihtiyaç duymadıkları bağımlılıkları kaldırabildiğinden derlemelerimiz artık çok daha hızlı.
Her zamanki gibi, katı geçişli bağımlılıkların uygulanması bir ödünleşme gerektiriyordu. Sık kullanılan kitaplıkların artık tesadüfen eklenmek yerine birçok yerde açıkça listelenmesi gerektiğinden derleme dosyaları daha ayrıntılı hale geldi ve mühendislerin BUILD
dosyalarına bağımlılık eklemek için daha fazla çaba harcaması gerekti. O zamandan beri, birçok eksik bağımlılığı otomatik olarak algılayıp geliştirici müdahalesi olmadan BUILD
dosyalarına ekleyerek bu zahmeti azaltan araçlar geliştirdik. Ancak bu tür araçlar olmadan bile, kod tabanı ölçeklendikçe bu değişimin faydalı olduğunu gördük: BUILD
dosyasına açıkça bağımlılık eklemek tek seferlik bir maliyettir ancak örtülü geçişli bağımlılıklarla uğraşmak, derleme hedefi var olduğu sürece devam eden sorunlara neden olabilir. Bazel, varsayılan olarak Java kodunda katı geçişli bağımlılıkları zorunlu kılar.
Harici bağımlılıklar
Bağımlılıklar dahili değilse harici olmalıdır. Dış bağımlılıklar, derleme sistemi dışında oluşturulan ve depolanan yapay nesnelerdeki bağımlılıklardır. Bağımlılık doğrudan bir yapay ürün deposundan (genellikle internet üzerinden erişilir) içe aktarılır ve kaynaktan oluşturulmak yerine olduğu gibi kullanılır. Dış ve iç bağımlılıklar arasındaki en büyük farklardan biri, dış bağımlılıkların sürümlerinin olması ve bu sürümlerin projenin kaynak kodundan bağımsız olarak bulunmasıdır.
Otomatik ve manuel bağımlılık yönetimi
Derleme sistemleri, harici bağımlılıkların sürümlerinin manuel veya otomatik olarak yönetilmesine izin verebilir. Manuel olarak yönetildiğinde derleme dosyası, yapıt deposundan indirmek istediği sürümü açıkça listeler. Genellikle 1.1.4
gibi bir anlamsal sürüm dizesi kullanılır. Kaynak dosya otomatik olarak yönetildiğinde kabul edilebilir sürümlerin aralığını belirtir ve derleme sistemi her zaman en son sürümü indirir. Örneğin, Gradle, bağımlılık sürümünün "1.+" olarak bildirilmesine izin vererek ana sürüm 1 olduğu sürece bağımlılığın herhangi bir küçük veya yama sürümünün kabul edilebilir olduğunu belirtir.
Otomatik olarak yönetilen bağımlılıklar küçük projeler için uygun olabilir ancak genellikle önemsiz olmayan büyüklükteki veya birden fazla mühendis tarafından üzerinde çalışılan projelerde felakete yol açar. Otomatik olarak yönetilen bağımlılıklarla ilgili sorun, sürümün ne zaman güncelleneceği konusunda kontrolünüzün olmamasıdır. Harici tarafların, anlamsal sürüm oluşturma kullandıklarını iddia etseler bile, uyumluluğu bozan güncellemeler yapmayacaklarını garanti etmenin bir yolu yoktur. Bu nedenle, bir gün çalışan bir derleme, ertesi gün bozulabilir ve neyin değiştiğini tespit etmenin veya derlemeyi çalışan bir duruma geri döndürmenin kolay bir yolu olmayabilir. Derleme bozulmasa bile, izlenmesi imkansız olan ince davranış veya performans değişiklikleri olabilir.
Buna karşılık, manuel olarak yönetilen bağımlılıklar kaynak kontrolünde değişiklik gerektirdiğinden kolayca bulunup geri alınabilir. Ayrıca, eski bağımlılıklarla derleme yapmak için deponun eski bir sürümünü kullanmak mümkündür. Bazel, tüm bağımlılıkların sürümlerinin manuel olarak belirtilmesini gerektirir. Orta ölçeklerde bile manuel sürüm yönetiminin getirdiği ek yük, sağladığı kararlılık açısından kesinlikle değerlidir.
Tek Sürüm Kuralı
Bir kitaplığın farklı sürümleri genellikle farklı yapılarla temsil edilir. Bu nedenle, aynı harici bağımlılığın farklı sürümlerinin, derleme sisteminde farklı adlar altında bildirilmemesi için teorik olarak bir neden yoktur. Bu sayede her hedef, bağımlılığın hangi sürümünü kullanmak istediğini seçebilir. Bu durum uygulamada birçok soruna yol açtığı için Google, kod tabanımızdaki tüm üçüncü taraf bağımlılıkları için katı bir Tek Sürüm Kuralı uygular.
Birden fazla sürüme izin vermeyle ilgili en büyük sorun, elmas bağımlılığı sorunudur. A hedefinin B hedefine ve harici bir kitaplığın v1 sürümüne bağlı olduğunu varsayalım. B hedefi daha sonra aynı harici kitaplığın v2 sürümüne bağımlılık eklemek için yeniden düzenlenirse A hedefi bozulur. Bunun nedeni, A hedefinin artık aynı kitaplığın iki farklı sürümüne örtülü olarak bağımlı olmasıdır. Hedefteki kullanıcıların farklı bir sürüme bağımlı olma ihtimali olduğundan, birden fazla sürümü olan herhangi bir üçüncü taraf kitaplığından yeni bir bağımlılık eklemek hiçbir zaman güvenli değildir. Tek Sürüm Kuralı'na uyulduğunda bu çakışma imkansız hale gelir. Bir hedef, üçüncü taraf kitaplığına bağımlılık eklerse mevcut bağımlılıklar zaten aynı sürümde olacağından sorunsuz bir şekilde birlikte var olabilir.
Geçişli dış bağımlılıklar
Dış bağımlılığın geçişli bağımlılıklarıyla uğraşmak özellikle zor olabilir. Maven Central gibi birçok yapay ürün deposu, yapay ürünlerin depodaki diğer yapay ürünlerin belirli sürümlerine bağımlılık belirtmesine olanak tanır. Maven veya Gradle gibi derleme araçları genellikle her geçişli bağımlılığı varsayılan olarak yinelemeli şekilde indirir. Bu nedenle, projenize tek bir bağımlılık eklemek toplamda düzinelerce yapının indirilmesine neden olabilir.
Bu çok kullanışlıdır: Yeni bir kitaplığa bağımlılık eklerken, bu kitaplığın her bir geçişli bağımlılığını bulup hepsini manuel olarak eklemek büyük bir zorluk olurdu. Ancak bu yöntemin büyük bir dezavantajı da vardır: Farklı kitaplıklar, aynı üçüncü taraf kitaplığının farklı sürümlerine bağlı olabileceğinden bu strateji, Tek Sürüm Kuralı'nı zorunlu olarak ihlal eder ve elmas bağımlılığı sorununa yol açar. Hedefiniz, aynı bağımlılığın farklı sürümlerini kullanan iki harici kitaplığa bağlıysa hangisini alacağınız belli olmaz. Bu durum, harici bir bağımlılığı güncellemenin, yeni sürüm bazı bağımlılıklarının çakışan sürümlerini çekmeye başlarsa kod tabanında görünüşte alakasız hatalara neden olabileceği anlamına da gelir.
Bazel, geçişli bağımlılıkları otomatik olarak indirmezdi. Geçmişte, tüm geçişli bağımlılıkların listelenmesini gerektiren bir WORKSPACE
dosyası kullanılıyordu. Bu durum, harici bağımlılıkların yönetimi sırasında çok fazla sorun yaşanmasına neden oluyordu. Bazel, MODULE.bazel
dosyası şeklinde otomatik geçişli harici bağımlılık yönetimi için destek ekledi. Daha fazla bilgi için external dependency
overview başlıklı makaleyi inceleyin.
Burada da yine kolaylık ve ölçeklenebilirlik arasında bir seçim yapmanız gerekir. Küçük projeler, geçişli bağımlılıkları yönetmekle uğraşmak istemeyebilir ve otomatik geçişli bağımlılıkları kullanabilir. Bu strateji, kuruluş ve kod tabanı büyüdükçe daha az çekici hale gelir. Çakışmalar ve beklenmedik sonuçlar giderek daha sık görülür. Daha büyük ölçeklerde, bağımlılıkları manuel olarak yönetmenin maliyeti, otomatik bağımlılık yönetiminin neden olduğu sorunlarla uğraşmanın maliyetinden çok daha düşüktür.
Harici bağımlılıkları kullanarak derleme sonuçlarını önbelleğe alma
Dış bağımlılıklar genellikle kitaplıkların kararlı sürümlerini yayınlayan üçüncü taraflarca sağlanır. Bu taraflar, kaynak kodu sağlamayabilir. Bazı kuruluşlar, kendi kodlarının bir kısmını yapı olarak kullanıma sunmayı da tercih edebilir. Bu sayede diğer kod parçaları, dahili bağımlılıklar yerine üçüncü taraf bağımlılıkları olarak bu kodlara bağlı olabilir. Bu özellik, yapılar yavaş oluşturuluyor ancak hızlı indiriliyorsa teorik olarak derleme hızını artırabilir.
Ancak bu durum, çok fazla ek yük ve karmaşıklığa da yol açar: Bu yapıların her birini oluşturup yapı deposuna yüklemekten birinin sorumlu olması gerekir ve istemcilerin en son sürümü kullandığından emin olması gerekir. Sistemin farklı bölümleri depodaki farklı noktalardan oluşturulacağından ve kaynak ağacının tutarlı bir görünümü olmayacağından hata ayıklama da çok daha zor hale gelir.
Yapıların oluşturulmasının uzun sürmesi sorununu çözmenin daha iyi bir yolu, daha önce açıklandığı gibi uzaktan önbelleğe almayı destekleyen bir derleme sistemi kullanmaktır. Bu tür bir derleme sistemi, her derlemeden elde edilen çıktıları mühendisler arasında paylaşılan bir konuma kaydeder. Bu nedenle, bir geliştirici başka biri tarafından yakın zamanda oluşturulan bir çıktıya bağlıysa derleme sistemi, bu çıktıyı oluşturmak yerine otomatik olarak indirir. Bu, doğrudan yapay nesnelere bağlı olmanın tüm performans avantajlarını sunarken derlemelerin her zaman aynı kaynaktan oluşturulmuş gibi tutarlı olmasını sağlar. Bu, Google tarafından şirket içinde kullanılan stratejidir ve Bazel, uzak önbellek kullanacak şekilde yapılandırılabilir.
Harici bağımlılıkların güvenliği ve güvenilirliği
Üçüncü taraf kaynaklarındaki eserlere güvenmek doğası gereği risklidir. Üçüncü taraf kaynağı (ör. yapay nesne deposu) çökerse kullanılabilirlik riski oluşur. Çünkü harici bir bağımlılığı indiremezse derlemenizin tamamı durabilir. Güvenlik riski de vardır: Üçüncü taraf sistem bir saldırgan tarafından ele geçirilirse saldırgan, referans verilen yapıyı kendi tasarımıyla değiştirebilir ve derlemenize rastgele kod yerleştirebilir. Her iki sorun da, bağımlı olduğunuz tüm yapıları kontrol ettiğiniz sunuculara yansıtarak ve derleme sisteminizin Maven Central gibi üçüncü taraf yapı depolarına erişmesini engelleyerek azaltılabilir. Bu yansıtma sitelerinin bakımı için çaba ve kaynak gerekir. Bu nedenle, bunları kullanıp kullanmama seçimi genellikle projenin ölçeğine bağlıdır. Güvenlik sorunu, her üçüncü taraf yapıtının karma değerinin kaynak deposunda belirtilmesini zorunlu kılarak da tamamen önlenebilir. Bu durumda, yapıt üzerinde değişiklik yapılırsa derleme başarısız olur. Bu sorunu tamamen ortadan kaldıran başka bir alternatif ise projenizin bağımlılıklarını satıcıya göndermektir. Bir proje, bağımlılıklarını tedarik ettiğinde bunları, projenin kaynak koduyla birlikte kaynak kontrolüne kaynak veya ikili dosyalar olarak kaydeder. Bu, projenin tüm harici bağımlılıklarının dahili bağımlılıklara dönüştürüldüğü anlamına gelir. Google, bu yaklaşımı dahili olarak kullanır ve Google'da referans verilen her üçüncü taraf kitaplığını Google'ın kaynak ağacının kökündeki third_party
dizinine kontrol eder. Ancak bu yöntem yalnızca Google'da işe yarar. Bunun nedeni, Google'ın kaynak denetim sisteminin son derece büyük bir tek depoyu işleyecek şekilde özel olarak oluşturulmuş olmasıdır. Bu nedenle, üçüncü taraf yazılımları kullanmak tüm kuruluşlar için bir seçenek olmayabilir.