Önceki sayfalara göz atarken bir temanın tekrar tekrar ortaya çıktığını göreceksiniz: Kendi kodunuzu yönetmek oldukça kolaydır ancak bağımlılıkları yönetmek çok daha zordur. Çeşitli bağımlılık türleri vardır: Bazen bir göreve (ör. "Sürümün tamamlandığını işaretlemeden önce dokümanları gönderin") bazen de bir yapıya (ör. "Kodu derlemek için bilgisayar görüşü 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, bazen de başka bir ekibin (kuruluşunuzda veya üçüncü taraf) sahip olduğu koda ya da verilere harici bağımlılıklarınız vardır. Ancak her durumda, "Buna sahip olmak için önce şuna ihtiyacım var" fikri, derleme sistemlerinin tasarımında tekrar tekrar ortaya çıkan bir şeydir ve bağımlılık yönetimi, belki de bir derleme sisteminin en temel işlevidir.
Modüller ve Bağımlılıklarla Başa Çıkma
Bazel gibi yapıya dayalı derleme sistemleri kullanan projeler, bir dizi modüle ayrılır. Bu modüller, BUILD
dosyaları aracılığıyla birbirlerine olan bağımlılıkları ifade eder. Bu modüllerin ve bağımlılıkların doğru şekilde düzenlenmesi, hem derleme sisteminin performansı hem de sürdürülmesi için gereken iş yükü üzerinde büyük bir etkiye sahip olabilir.
Ayrıntılı Modülleri ve 1:1:1 Kuralını Kullanma
Yapıya dayalı bir derleme oluştururken ilk soru, tek bir modülün ne kadar işlev 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 söz konusu projenin tüm kaynak dosyaları yinelenen bir şekilde gruplandırılarak projenin tamamı tek bir modüle dahil edilebilir. Diğer uçta ise neredeyse her kaynak dosya kendi modülüne dönüştürülebilir. Bu durumda, her dosyanın bir BUILD
dosyasında bağımlı olduğu diğer tüm dosyaları listelemesi gerekir.
Çoğu proje bu iki uç nokta arasında bir yerdedir 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ç dokunmanız gerekmediği anlamına gelebilir ancak bu durumda derleme sisteminin her zaman projenin tamamını tek seferde oluşturması gerekir. Bu, derlemenin bazı bölümlerini paralelleştiremeyeceği veya dağıtamayacağı, ayrıca daha önce derlenmiş bölümleri önbelleğe alamayacak anlamına gelir. Dosya başına bir modülde ise durum tam tersidir: Derleme sistemi, derlemenin önbelleğe alma ve planlama adımlarında maksimum esnekliğe sahiptir ancak mühendislerin, hangi dosyaların hangisine referans verdiğini değiştirdiklerinde bağımlılık listelerini korumak için daha fazla çaba göstermesi gerekir.
Tam ayrıntı düzeyi dile göre değişse de (ve genellikle aynı dilin içinde bile) Google, görev tabanlı bir derleme sisteminde genellikle yazılabilecekten çok daha küçük modülleri tercih eder. Google'daki tipik bir üretim ikili dosyası genellikle on binlerce hedefe bağlıdır ve orta büyüklükte bir ekip bile kod tabanında birkaç yüz hedefe sahip olabilir. Java gibi güçlü bir yerleşik paketleme kavramına sahip diller için her dizin genellikle tek bir paket, hedef ve BUILD
dosyası içerir (Bazel'e dayalı başka bir derleme sistemi olan Pants, buna 1:1:1 kuralı adını verir). Paketleme kuralları daha zayıf olan diller genellikle BUILD
dosyası başına birden fazla hedef tanımlar.
Daha küçük derleme hedeflerinin avantajları, daha hızlı dağıtılan derlemelere ve hedefleri yeniden oluşturma ihtiyacının daha seyrek olmasına yol açtığı için ölçekte gerçekten kendini göstermeye başlar.
Daha ayrıntılı hedefler, derleme sisteminin yalnızca belirli bir değişiklikten etkilenebilecek sınırlı sayıda test çalıştırması anlamına geldiğinden, test devreye girdikten sonra avantajlar daha da ilgi çekici hale gelir. Google, daha küçük hedefler kullanmanın sistemsel avantajlarına inandığından, geliştiricilerin yükünü hafifletmek için BUILD
dosyalarını otomatik olarak yönetecek araçlara yatırım yaparak olumsuz yönleri azaltma konusunda bazı adımlar attık.
buildifier
ve buildozer
gibi bu araçlardan bazıları, Bazel'de buildtools
dizininde 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. Bu görünürlük, diğer hangi hedeflerin bu hedefe bağlı olabileceğini belirleyen bir özelliktir. Gizli hedeflere yalnızca kendi BUILD
dosyalarında referans verilebilir. Bir hedef, açıkça tanımlanmış bir BUILD
dosyası listesinin hedeflerine veya herkese açık görünürlük durumunda, çalışma alanındaki her hedefe daha geniş görünürlük verebilir.
Çoğu programlama dilinde olduğu gibi, görünürlüğü mümkün olduğunca en aza indirmek genellikle en iyi seçenektir. 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ğer kullanıcıların kendileriyle koordine olmasını gerektiren ekiplerin, hedeflerinin görünürlüğü olarak müşteri hedeflerinin izin verilenler listesi bulunur. Her ekibin dahili uygulama hedefleri yalnızca ekibin sahip olduğu dizinlerle sınırlı olacak ve çoğu BUILD
dosyasının gizli olmayan yalnızca bir hedefi olacaktır.
Bağımlılıkları yönetme
Modüllerin birbirine referans vermesi 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ın ifade edilmesi, genellikle bir BUILD
dosyasındaki içeriğin büyük kısmını oluşturur.
Dahili bağımlılıklar
İnce ayrıntılı modüllere ayrılmış büyük bir projede, bağımlılıkların çoğunun dahili olması muhtemeldir. Yani aynı kaynak depoda tanımlanan ve oluşturulan başka bir hedeftedir. İç bağımlılıklar, derleme işlemi sırasında önceden derlenmiş bir yapı olarak indirilmek yerine kaynaktan derlendikleri için harici bağımlılıklardan farklıdır. Bu aynı zamanda, dahili bağımlılar için "sürüm" kavramının olmadığı anlamına da gelir. Bir hedef ve tüm dahili bağımlılıkları, depodaki aynı taahhüt/düzeltme ile her zaman derlenir. Dahili bağımlılıklarla ilgili olarak dikkatle ele alınması gereken bir konu, geçişli bağımlılıkların nasıl ele alınacağıdır (Şekil 1). A hedefinin, ortak bir kitaplık hedefi C'ye bağlı olan B hedefine bağlı olduğunu varsayalım. A hedefi, C hedefinde tanımlanan sınıfları kullanabilmeli mi?
Şekil 1. Geçiş bağımlılıkları
Temel araçlar açısından bu konuda bir sorun yoktur; hem B hem de C, derlendiğinde hedef A'ya bağlanır. Bu nedenle, C'de tanımlanan tüm semboller A tarafından bilinir. Bazel bu duruma yıllarca izin verdi ancak Google büyüdükçe sorunlar yaşamaya başladık. B'nin, artık C'ye bağımlı olması gerekmeyecek şekilde yeniden yapılandırıldığını varsayalım. Ardından B'nin C'ye olan bağımlılığı kaldırılırsa A ve B'ye olan bağımlılığı aracılığıyla C'yi kullanan diğer tüm hedefler bozulur. Bu nedenle, bir hedefin bağımlılıkları, herkese açık sözleşmesinin 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şlamaya başlamasına neden oldu.
Google, sonunda Bazel'de "katı geçişli bağımlılık modu"nu tanıtarak bu sorunu çözdü. Bu modda Bazel, bir hedefin bir sembole doğrudan bağımlı olmadan referans verip vermediğini algılar ve bu durumda bir hata ve bağımlılığı otomatik olarak eklemek için kullanılabilecek bir kabuk komutu ile 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ı açıkça listeleyecek şekilde yeniden yapılandırmak yıllar süren bir çalışmaydı ancak buna değdi. Hedeflerin gereksiz bağımlılıkları daha az olduğu için derlemelerimiz artık çok daha hızlı. Ayrıca mühendisler, kendilerine bağlı hedeflerin bozulmasından endişe etmeden ihtiyaç duymadıkları bağımlılıkları kaldırabiliyor.
Her zaman olduğu gibi, katı geçişli bağımlılıkları zorunlu kılmak da bir denge gerektiriyordu. Sık kullanılan kitaplıkların artık tesadüfen dahil edilmek yerine birçok yerde açıkça listelenmesinin gerekmesi ve mühendislerin BUILD
dosyalarına bağımlılıklar eklemek için daha fazla çaba harcaması gerektiğinden, derleme dosyaları daha ayrıntılı hale geldi. O zamandan beri, eksik bağımlılıkların çoğunu otomatik olarak tespit edip 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 dengenin buna değer olduğunu tespit ettik: BUILD
dosyasına açıkça bağımlılık eklemek tek seferlik bir maliyettir ancak dolaylı geçişli bağımlılıklarla uğraşmak, derleme hedefi var olduğu sürece sürekli 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
Bir bağımlılık dahili değilse harici olmalıdır. Dış bağımlılıklar, derleme sistemi dışında oluşturulup depolanan yapılardır. Bağımlılık doğrudan bir yapı deposundan (genellikle internet üzerinden erişilir) içe aktarılır ve kaynaktan derlenmek 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 var olması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, yapı dosyası, genellikle 1.1.4
gibi bir anlamsal sürüm dizesi kullanarak yapı deposundan indirmek istediği sürümü açıkça listeler. Otomatik olarak yönetildiğinde kaynak dosya, kabul edilebilir sürümlerin bir aralığını belirtir ve derleme sistemi her zaman en son sürümü indirir. Örneğin, Gradle, 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 belirtmek için bağımlılığın sürümünün "1.+" olarak tanımlanmasına olanak tanır.
Otomatik olarak yönetilen bağımlılıklar küçük projeler için uygun olabilir ancak genellikle önemli boyutlarda veya birden fazla mühendisin üzerinde çalıştığı projelerde felaketle sonuçlanır. Otomatik olarak yönetilen bağımlılıklarla ilgili sorun, sürümün ne zaman güncelleneceği üzerinde hiçbir kontrole sahip olmamanızdır. Harici tarafların, anlamsal sürüm kontrolünü kullandıklarını iddia etseler bile çalışmayı durduran güncellemeler yapmayacaklarını garanti etmek mümkün değildir. Bu nedenle, bir gün çalışan bir derleme, ertesi gün çalışmayı durdurabilir ve neyin değiştiğini tespit etmenin veya derlemeyi çalışır duruma geri döndürmenin kolay bir yolu yoktur. Derleme bozulmasa bile, tespit edilmesi imkansız olan küçük davranış veya performans değişiklikleri olabilir.
Buna karşılık, manuel olarak yönetilen bağımlılıklar kaynak denetiminde bir değişiklik gerektirdiğinden kolayca bulunabilir ve geri alınabilir. Ayrıca, eski bağımlılıklarla derleme yapmak için kod deposunun eski bir sürümüne göz atabilirsiniz. Bazel, tüm bağımlılıkların sürümlerinin manuel olarak belirtilmesini gerektirir. Orta ölçekte bile manuel sürüm yönetiminin sağladığı kararlılık için bu işleme harcanan zamana değer.
Tek Sürüm Kuralı
Bir kütüphanenin farklı sürümleri genellikle farklı yapı taşlarıyla temsil edilir. Bu nedenle, teorik olarak aynı harici bağımlılık için farklı sürümlerin derleme sisteminde farklı adlar altında tanımlanmasının bir sakıncası yoktur. Böylece her hedef, kullanmak istediği bağımlılık sürümünü 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ı uyguluyor.
Birden fazla sürüme izin vermenin en büyük sorunu, elmas bağımlılığı sorunudur. A hedefinin B hedefine ve harici bir kitaplığın 1. 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 yapılandırılırsa A hedefi artık aynı kitaplığın iki farklı sürümüne dolaylı olarak bağımlı olduğu için çalışmaz. Hedefin kullanıcılarından herhangi biri zaten farklı bir sürüme bağlı olabileceğinden, bir hedeften birden fazla sürümü olan herhangi bir üçüncü taraf kitaplığına yeni bir bağımlılık eklemek hiçbir zaman güvenli değildir. Tek Sürüm Kuralı'na uymak bu çakışmayı imkansız kılar. Bir hedef, üçüncü taraf kitaplığa bağımlılık eklerse mevcut bağımlılıklar zaten aynı sürümde olur. Böylece, bu bağımlılıklar birlikte sorunsuz bir şekilde çalışabilir.
Geçişli harici bağımlılıklar
Bir dış bağımlığın geçişli bağımlılıklarıyla başa çıkmak özellikle zor olabilir. Maven Central gibi birçok yapı deposu, yapıların depodaki diğer yapıların belirli sürümlerine olan bağımlılıkları belirtmesine olanak tanır. Maven veya Gradle gibi derleme araçları genellikle varsayılan olarak her geçişli bağımlılığı yinelemeli olarak indirir. Bu, projenize tek bir bağımlılık eklemenin toplamda düzinelerce yapının indirilmesine neden olabileceği anlamına gelir.
Bu çok kullanışlıdır: Yeni bir kitaplığa bağımlılık eklerken, söz konusu kitaplığın geçişli bağımlılıklarının her birini bulup hepsini manuel olarak eklemek çok zor olur. Ancak bunun 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, bir 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 hangi sürümün kullanılacağı belli olmaz. Bu durum, harici bir bağımlılığın güncellenmesi durumunda yeni sürüm, bağımlılığının bazılarının çakışan sürümlerini çekmeye başlarsa kod tabanında birbiriyle alakalı olmayan hatalar oluşabileceği anlamına da gelir.
Bu nedenle Bazel, geçişli bağımlılıkları otomatik olarak indirmez.
Maalesef bu sorunu çözmek için tek bir çözüm yok. Bazel'in alternatifi, deponun harici bağımlılıklarının her birini ve depo genelinde bu bağımlılık için kullanılan açık bir sürümü listeleyen bir genel dosya gerektirmektir. Neyse ki Bazel, bir Maven yapı grubuna ait geçişli bağımlılıkları içeren bu tür bir dosyayı otomatik olarak oluşturabilen araçlar sağlar. Bu araç, bir projenin ilk WORKSPACE
dosyasını oluşturmak için bir kez çalıştırılabilir. Ardından, her bağımlılığın sürümlerini ayarlamak için bu dosya manuel olarak güncellenebilir.
Burada da seçim, kolaylık ve ölçeklenebilirlik arasındadır. Küçük projelerde, geçişli bağımlılıkları yönetmek zorunda kalmamak tercih edilebilir ve otomatik geçişli bağımlılıklar kullanılabilir. Kuruluş ve kod tabanı büyüdükçe ve çakışmalar ile beklenmedik sonuçlar giderek daha sık ortaya çıktıkça bu stratejinin cazibesi de azalır. Daha büyük ölçekte, bağımlılıkların manuel olarak yönetilmesinin maliyeti, otomatik bağımlılık yönetiminin neden olduğu sorunlarla uğraşmanın maliyetinden çok daha azdı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ü taraflar tarafından sağlanır. Bu taraflar, kaynak kodu sağlamayabilir. Bazı kuruluşlar, kendi kodlarının bir kısmını yapı olarak sunmayı da seçebilir. Bu sayede diğer kod parçaları, şirket içi bağımlılıklar yerine üçüncü taraf olarak bu yapılara bağımlı olabilir. Yapıların derlenmesi yavaş ancak indirilmesi hızlıysa bu, teorik olarak derlemeleri hızlandırabilir.
Ancak bu, çok fazla ek yük ve karmaşıklık da getirir: Bu yapıların her birini oluşturmaktan ve yapı deposuna yüklemekten birinin sorumlu olması gerekir. Ayrıca istemcilerin en son sürümle güncel kalmasını sağlaması gerekir. Sistemin farklı bölümleri, depoda farklı noktalardan derleneceği ve artık kaynak ağacın tutarlı bir görünümü olmadığı için hata ayıklama da çok daha zor hale gelir.
Yapımlarını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 yapıları mühendisler arasında paylaşılan bir konuma kaydeder. Bu nedenle, bir geliştirici yakın zamanda başka biri tarafından oluşturulan bir yapıya ihtiyaç duyarsa derleme sistemi, yapıyı oluşturmak yerine otomatik olarak indirir. Bu, doğrudan yapı ürünlerine bağlı olmanın tüm performans avantajlarını sağlarken derlemelerin her zaman aynı kaynaktan derlenmiş gibi tutarlı olmasını sağlar. Bu, Google tarafından şirket içinde kullanılan stratejidir ve Bazel, uzak önbelleği kullanacak şekilde yapılandırılabilir.
Harici bağımlılıkların güvenliği ve güvenilirliği
Üçüncü taraf kaynaklarından gelen yapıların kullanılması doğal olarak risklidir. Üçüncü taraf kaynağı (ör. yapı deposu) devre dışı kalırsa kullanılabilirlik riski vardır. Çünkü harici bir bağımlılık indiremezse derlemenizin tamamı durabilir. Ayrıca güvenlik riski de vardır: Üçüncü taraf sisteminin güvenliği bir saldırgan tarafından ihlal edilirse saldırgan, referans verilen yapıyı kendi tasarımlarından biriyle değiştirebilir ve derlemenize keyfi 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. Bununla birlikte, bu yansıtıcıların bakımı için çaba ve kaynak gerekir. Bu nedenle, bunları kullanıp kullanmama kararı genellikle projenin ölçeğine bağlıdır. Güvenlik sorunu, her üçüncü taraf yapının karmasının kaynak depoda belirtilmesini zorunlu kılarak da çok az ek yük ile tamamen önlenebilir. Bu durumda, yapıda değişiklik yapılması derlemenin başarısız olmasına neden olur. Sorunu tamamen atlatan bir diğer alternatif de projenizin bağımlılıklarını tedarikçiye aktarmaktır. Bir proje, bağımlılıkları tedarikçiye gönderirken bunları projenin kaynak koduyla birlikte kaynak veya ikili kod olarak kaynak denetimine gönderir. 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 bir third_party
dizininde kontrol eder. Ancak bu, yalnızca Google'ın kaynak denetimi sisteminin son derece büyük bir monorepoyu işleyebilecek şekilde özel olarak tasarlanmış olması nedeniyle Google'da işe yarar. Bu nedenle, tedarikçi firmalardan hizmet almak tüm kuruluşlar için uygun olmayabilir.