İçindekiler
- Sorun
- Tasarım kararları ve alternatifler
- Üç boyut, birbirinden ayrı
- Kütüphanede bağımlılık yok
- İnsan ve CI için çıktı
- Girdi güvenilmez
- Gerçek bir hata: Swift string'leri dosya yolu değildir
- Kullanım
- Sınırlar
Sorun
Bir iOS uygulaması her sürümde biraz büyür ve "onu ne büyüttü?" sorusu genellikle .ipa'yı açıp dosyaları boyuta göre sıralayarak cevaplanır. Bu, sorunun yalnız bir kısmını cevaplar. Bir framework birçok dosyadan oluşan bir klasördür; bir dil, uygulamaya ve framework'lerine dağılmış .lproj klasörlerinin toplamıdır; universal bir binary her mimari için ayrı bir slice taşır; asset catalog ise tek ve içi görünmeyen bir Assets.car dosyasıdır. Apple'ın kendi sayıları, yani Xcode'un App Thinning Size Report'u ve App Store Connect'teki boyutlar, Apple'ın derleme hattını ve imzalı bir build'i gerektirir. Bu yüzden bir pull request kontrolünün Linux runner'da üretebileceği şeyler değildir.
sizeledger bu boşluk için yazılmış bir komut satırı aracı ve Swift kütüphanesidir. Bir .ipa, .app ya da .xcarchive verdiğinizde byte'ların nerede olduğunu gösterir: kategoriye göre (ana çalıştırılabilir dosya, framework'ler, app extension'lar, App Clip'ler, watch uygulaması, yerelleştirmeler, asset catalog'lar, kod imzası, diğer kaynaklar), framework ve kaynağa göre, .lproj diline göre ve CPU mimarisi slice'ına göre. Yalnızca dosyaları okur. Hiçbir şeyi derlemez, çalıştırmaz, yüklemez ya da değiştirmez; macOS'ta ve Linux'ta aynı raporu verir.
Repo 15 Ekim 2026'da açıldı ve 0.1.0 aynı gün yayınlandı. 15–21 Ekim arasında on yama sürümü daha çıktı; güncel sürüm 0.1.10. Bunların neredeyse hepsi okuyucuları özel hazırlanmış (crafted) girdilere karşı sağlamlaştırıyor; bu yazıdaki derslerin çoğu da oradan geliyor.
Tasarım kararları ve alternatifler
Üç boyut, birbirinden ayrı
En önemli karar neyin iddia edilmeyeceğiydi. "Uygulama boyutu" en az üç farklı anlama gelir ve sizeledger her birini kendi adıyla raporlar:
- Toplam (
bytes): dosyaların boyutlarının toplamı, bir dosya yöneticisinin gösterdiği değer. - Kurulum boyutu, tahmin (
installBytes): her dosya, APFS'nin sakladığı gibi tam 4 KiB bloklara yukarı yuvarlanır. Kaynakta buInstallSize.estimate'tir veblockSize4.096'dır. .ipaiçinde sıkıştırılmış (compressedBytes): bundle dosyalarının arşiv içinde kapladığı yer; yalnız.ipagirdisinde.
Hiçbiri App Store'un indirme ya da kurulum boyutu değildir. App Store uygulamayı cihaz başına inceltir (tek mimari slice, yalnız o cihazın asset görselleri), binary'leri şifreler ve indirmeyi sıkıştırır. Alternatif, yüklenen dosyadan mağaza sayısını tahmin eden bir formül olurdu. Ne kadar yanıldığı bilinmeyen bir tahmin, ne olduğunu dürüstçe söyleyen yerel bir sayıdan kötüdür. Bu yüzden rapor kurulum boyutuna "tahmin" der ve girdi bir simülatör build'iyse ya da universal binary içeriyorsa not ekler; bunlar bir cihazın daha küçük bir şey görmesinin en yaygın iki nedenidir.
Tahmin varsayılmıyor, ölçülüyor. Testler iki SampleShop fixture uygulamasını açıp du ile karşılaştırır: bir Mac'te (Apple silicon, macOS 27.0.1, APFS) kurulum tahmini du -sk ile birebir aynıdır ve toplam du -A'nın %0,16 yakınındadır; Linux konteynerinde (swift:6.4.1-noble, aarch64) toplam du -b --apparent-size ile aynıdır, kurulum tahmini ise orada klasör başına da 4 KiB sayan du -sk'nın %0,65 yakınındadır. Testler iki ortamda da %1'in üstünde kırılır.
Kütüphanede bağımlılık yok
SizeLedgerKit'in hiçbir paket bağımlılığı yok. ZIP okuyucu (ZIP64 dahil), DEFLATE çözücü ve Mach-O ayrıştırıcı (thin, fat ve fat64) kütüphanenin içinde Swift ile yazıldı. Akla gelen alternatif unzip, lipo ve otool çağırmaktır; ama lipo ve otool Xcode ile gelir. Aracın amacı ise ölçümün her yerde, macOS build işinden .ipa'yı artifact olarak alan bir Linux CI işinde bile çalışabilmesidir. Okuyucuları Swift ile yazmak, aracın okuduğu her byte'ın testlerin kapsadığı koddan geçmesi demek: DEFLATE çözücü stored, fixed ve dynamic bloklar için zlib çıktısıyla, Mach-O ayrıştırıcı ise thin, 32-bit, fat ve fat64 dosyalarla sınanıyor. Yalnızca komut satırı hedefi swift-argument-parser'a bağımlı.
İnsan ve CI için çıktı
Varsayılan çıktı bir terminal tablosudur. --format markdown bir pull request yorumu ya da job summary için GitHub Markdown'ı yazar; --format json ise anahtar sırası sabit bir rapor üretir, böylece commit'lenebilir, jq ile sorgulanabilir ve diff ile karşılaştırılabilir. 1.0'a kadar JSON düzeni schemaVersion 0 taşır ve bir minor sürümde değişebilir; değiştiğinde changelog bunu söyler.
Girdi güvenilmez
Bir .ipa başkasının size verdiği bir dosyadır; sizeledger onu düşmanca kabul eder. 0.1.4'ten beri 500.000'den fazla dosya içeren, 64 klasörden derin yol barındıran, 8 GiB'tan fazla sıkıştırılmamış byte bildiren ya da saklanan boyutunun 300 katından fazlasına açılan girdiyi reddeder. Sınırlar bayraklarla (--max-files, --max-depth, --max-bytes, --max-ratio), kütüphanede ise SizeLedger.analyze(_:limits:)'e verilen bir AnalysisLimits değeriyle ayarlanır. 0.1.1, Mach-O başlıklarının okunma biçimini değiştirdi: fat başlığı bir slice'ı DEFLATE girdisinin 1 GB içine yerleştiren 1 MB'lık özel bir .ipa, 0.1.0'ın bellekte 3,77 GB tutmasına yol açıyordu. Başlıklar artık en fazla 4 KiB'lık aralıklar olarak okunuyor ve sıkıştırma çözülürken yalnız DEFLATE geri referanslarının ihtiyaç duyduğu 32 KiB'lık pencere tutuluyor; aynı dosya bundan sonra bir Mac'te (Apple silicon) 11,5 MB kullandı (PR #27).
Gerçek bir hata: Swift string'leri dosya yolu değildir
En öğretici hata boyutlarla ilgili değildi. Swift'in String'i kanonik olarak eşdeğer metni eşit sayar ve Character'e, yani grapheme cluster'a göre böler. Bir unzip aracı ve bir dosya sistemi bunların hiçbirini yapmaz. 0.1.7'ye kadar sizeledger girdi yollarını String olarak işliyordu ve PR #52 bunun neyi bozduğunu sıralıyor:
Payload/G.app/../\u{301}evilkabul ediliyordu, oysa README bir..bileşeninin reddedildiğini söylüyor. U+0301 birleştirici işareti önündeki/ile tek birCharacteroluşturuyor, bu yüzdensplit(separator: "/")hiçbir zaman..üretmiyordu.- 5 MB'lık
Payload/G.app/\u{301}big.bin,hasPrefix("Payload/G.app/")kontrolünü geçemiyor, bundle dışındaki dosyalara düşüyor ve toplamlardan çıkıyordu. - NFC ve NFD yazılmış
café.bintek birStringsözlük anahtarıydı; ikinci dosya birincinin yerine geçiyor ve 5 MB not düşülmeden toplamlardan kayboluyordu. - Ardından birleştirici işaret gelen bir
/,--max-depthsayımına girmiyordu.
Düzeltme (commit f77172f, "fix(ipa): split and compare entry paths on their bytes") hangi dosyaların var olduğu, hangi klasörde durduğu ve ne kadar derin olduğu kararlarının tümünü byte'lara taşıdı. İç BundlePath tipi utf8 görünümleri üzerinde böler, önek kontrol eder ve karşılaştırır; PathKey tipi ise sözlükleri ve kümeleri byte'larla anahtarlar. PathBytesTests içindeki sekiz yeni test, değişiklikten önce main üzerinde kırılıyordu.
İlk düzeltme hikâyenin tamamı değildi. 0.1.8'de (PR #55) adı geçerli UTF-8 olmayan bir .ipa girdisi hâlâ Latin-1 olarak çözülüp o metinle tanımlanıyordu; böylece caf\xE9.bin ve UTF-8 café.bin tek bir yol oluyordu: unzip -l'nin 4 dosya ve 5.000.179 B olarak listelediği bir arşiv 3 dosya ve 179 B olarak raporlanıyordu. Kimlik artık arşivin sakladığı byte'lardan geliyor (PathKey(bytes:)); gösterilen ad yine Latin-1 okuması, rapor da iki satırın aynı okunabileceğini not ediyor. İkisinden çıkardığım ders şu: bir yol, metin olarak gösterilen bir byte dizisidir ve String eşitliği, hasPrefix ya da String anahtarlı bir sözlük üzerinden yapılan her karşılaştırma, dosya sisteminin hiç vermediği bir kararı verir.
Kullanım
macOS ya da Linux'ta Homebrew ile kurun ya da macOS (universal) veya Linux (x86_64, aarch64) için bir release binary'si indirip SHA256SUMS ile doğrulayın:
sh
1brew install muhittincamdali/tap/sizeledgerSonra dışa aktarılmış bir .ipa'yı, bir build'den çıkan .app'i ya da bir .xcarchive'i ölçün:
sh
1sizeledger analyze path/to/MyApp.ipaSampleShop 2.0 fixture'ında bu komut 21 dosyada 8,75 MB, 8,80 MB kurulum tahmini ve 2,72 MB sıkıştırılmış boyut yazar; catalog.json uygulamanın %70,8'i, Assets.car %18,1'idir. CI için README yaygın durumları gösteriyor:
sh
1# Keep the report for later2sizeledger analyze build/MyApp.ipa --out size/main.json3 4# Markdown in a GitHub Actions job summary5sizeledger analyze build/MyApp.xcarchive --format markdown >> "$GITHUB_STEP_SUMMARY"0.1.10'dan beri --out var olan bir dosyanın üzerine yazmaz: --force verilmedikçe çıkış kodu 2 ile biter. Çıkış kodu 0 raporun üretildiğini, 2 girdinin okunamadığını ya da raporun yazılamadığını, 64 seçeneklerin geçersiz olduğunu söyler.
Kütüphane aynı raporu kendi araçlarınıza verir. .package(url: "https://github.com/muhittincamdali/sizeledger", from: "0.1.10") ve SizeLedgerKit ürününü ekleyin. Linux workflow'unun her pull request'te ve main'e her push'ta derleyip çalıştırdığı Examples/LibraryUsage örneği şöyle başlar:
swift
1let report: SizeReport2do {3 report = try SizeLedger.analyze(URL(fileURLWithPath: path))4} catch {5 print("error: \(Sanitize.text(String(describing: error)))")6 exit(2)7}8 9let totals = report.totals10print("\(Sanitize.text(report.app.name ?? report.input.bundle)): \(totals.files) files")11print(" bytes: \(ByteFormat.string(totals.bytes))")12print(" install size (estimate): \(ByteFormat.string(totals.installBytes))")Adlar, yollar ve notlar uygulamadan gelir; bu yüzden terminale ulaşmadan önce Sanitize.text'ten geçerler. Bu fonksiyon kontrol karakterlerini, çift yönlü metin override'larını ve görünmeyen karakterleri değiştirir.
Sınırlar
- Boyutlar yerel ölçümlerdir. App Store indirme ve kurulum boyutları Apple tarafındaki inceltme, şifreleme ve sıkıştırmaya bağlıdır; sizeledger bir tahmin verir ve bunu açıkça söyler.
- Asset catalog'lar dosya olarak sayılır.
Assets.cariçindeki rendition'lar henüz listelenmiyor (0.8 için planlandı, macOS'taassetutilile). - Swift metadata,
__swift5_*bölümlerinin boyutudur ve bir alt sınırdır, çünkü metadata'nın bir kısmı__constiçinde durur. - iOS tarzı bundle'lar sınıflandırılır.
Contents/klasörlü bir macOS.app'i ölçülür ama dosyaları "Other resources" altına düşer. - Birden fazla uygulama, şifreli girdi, stored ve DEFLATE dışında sıkıştırma ya da
..yol bileşeni içeren bir.ipatahmin yürütülmeden, bir mesajla reddedilir. - Varsayılan sınırlar içinde özel hazırlanmış bir girdi yine de zaman harcatabilir: bulunan en yavaşı, her biri 4,29 GB bildiren iki girdili 28,6 MB'lık bir dosya, başka işlerle meşgul bir Mac'te (Apple silicon) 15–24 sn sürdü. Daha düşük bir
--max-bytesbu sınırı düşürür. - Bugün yalnız
analyzevar. İki raporu karşılaştırma (diff, 0.2 için planlandı), GitHub Action ilebudgetkuralları (0.3 için planlandı), HTML treemap (0.8 için planlandı), dondurulmuş JSON şeması (1.0 için planlandı) ve Android.apk/.aabdesteği (1.1 için planlandı) yol haritasında, araçta değil.budgetgelene kadar CI'da sınır kontrolü.totals.installBytesüzerinde tek satırlık birjqkomutudur.
Test paketinde 159 test var; CI bunları macOS'ta ve Linux'ta (x86_64 ve arm64) çalıştırıyor. Biri UTF-8 olmayan bir dosya adına ihtiyaç duyuyor, APFS bunu kabul etmediği için yalnız Linux'ta koşuyor.
Kaynak kod: github.com/muhittincamdali/sizeledger
