Önceki sayfalara baktığımızda bir tema tekrar tekrar karşımıza çıkıyor: Kendi kodunuzu yönetmek oldukça kolay olsa da bağımlılıklarını yönetmek çok daha zor. Her türlü bağımlılık vardır: Bazen bir göreve (ör. "Yayınlamayı tamamlandı olarak işaretlemeden önce belgeleri gönderin") bazen de bir yapıta (ör. "Kodumu oluşturmak için bilgisayar 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ü taraftaki başka bir ekibe ait kod ya da verilere harici bağımlılıklarınız olur. 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 artefakta dayalı 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
Bir yapıyı yapılandırma sırasında 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 derlenebilir bir birimi belirten bir hedefle temsil edilir. Bir uç noktada, köke bir BUILD dosyası yerleştirilerek ve 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 iki uç nokta 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 bir 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ı aynı anda derlemesi gerekir. Bu nedenle, derlemenin bölümlerini paralelleştiremez veya dağıtamaz ya da daha önce 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 olandan ö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 hedef sahibi olabilir. Paketleme kavramı yerleşik olarak güçlü bir şekilde bulunan Java gibi dillerde her dizin genellikle tek bir paket, hedef ve BUILD dosyası içerir (Bazel'e dayalı başka bir derleme sistemi olan Pants, bunu 1:1:1 kuralı olarak adlandırır). 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österir.
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 hedefler kullanmanın 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 olarak kullanılabilir. 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 tüm ekiplerin 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 görünürlüğü olarak müşteri hedeflerinin izin verilenler listesini tutar. Her ekibin dahili uygulama hedefleri yalnızca ekibe ait dizinlerle sınırlı olacak ve çoğu BUILD dosyası yalnızca herkese açık bir hedef içerecek.
Bağımlılıkları Yönetme
Modüllerin birbirine referans verebilmesi 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ştirmeye 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ımlanıp oluşturulan 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 C kitaplık 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 ortaya çıkmaya başladı. 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ılık üzerinden 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 referans vermeye çalışıp çalışmadığını algılar. Eğer hedef böyle bir girişimde bulunursa 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 çalışma oldu ancak bu çabaya değdi. Hedeflerde artık 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 dahil edilmek 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 olmasa 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 tüm küçük veya yama sürümlerinin 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, semantik 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ü kullanabilirsiniz. Bazel, tüm bağımlılıkların sürümlerinin manuel olarak belirtilmesini gerektirir. Orta ölçekli projelerde 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 neden olduğundan 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. Hedef B daha sonra aynı harici kitaplığın v2'sine bağımlılık eklemek için yeniden düzenlenirse hedef A artık aynı kitaplığın iki farklı sürümüne örtülü olarak bağlı olduğundan bozulur. 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 olabilirler.
Geçişli harici 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 özellik çok kullanışlıdır: Yeni bir kitaplığa bağımlılık eklerken, bu kitaplığın geçişli bağımlılıklarının her birini bulup manuel olarak eklemek çok zahmetli 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ı 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, yeni sürüm bazı bağımlılıklarının çakışan sürümlerini çekmeye başlarsa harici bir bağımlılığı güncellemenin, 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çişli bağımlılıkların listelenmesini gerektiren bir WORKSPACE dosyası kullanılıyordu. Bu da 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 desteği ekledi. Daha fazla bilgi için Harici bağımlılığa genel bakış 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 sürümler kaynak kodu içermeyebilir. 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 derlemeleri teorik olarak hızlandı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 sorumlu birinin olması ve istemcilerin en son sürümü kullandıklarından emin olmaları 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, çıktıyı oluşturmak yerine otomatik olarak indirir. Bu, doğrudan yapay nesnelere bağlı olmanın tüm performans avantajlarını sağlarken derlemelerin her zaman aynı kaynaktan oluşturulmuş gibi tutarlı olmasını da sağlar. Bu, Google tarafından şirket içinde kullanılan stratejidir ve Bazel, uzak bir önbelleği kullanacak şekilde yapılandırılabilir.
Harici bağımlılıkların güvenliği ve güvenilirliği
Üçüncü taraf kaynaklarındaki yapılara bağlı olmak 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. Ayrıca bir 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 böylece derlemenize rastgele kod yerleştirebilir. Her iki sorun da, bağlı 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 depolarının 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ısının karmasının kaynak deposunda belirtilmesini zorunlu kılarak da tamamen önlenebilir. Bu durumda, yapı üzerinde değişiklik yapılırsa derleme başarısız olur. Bu sorunu tamamen ortadan kaldıran başka bir alternatif de projenizin bağımlılıklarını satıcıya bildirmektir. Bir proje bağımlılıklarını tedarik ettiğinde, bunları kaynak kodu veya ikili dosyalar olarak projenin kaynak koduyla birlikte kaynak kontrolüne 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 kaydeder. Ancak bu yöntem yalnızca Google'da işe yarar. Bunun nedeni, Google'ın kaynak kontrol 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.
