Bazel 8: Haqiqiy ilova anatomiyasi — resurslar, lokalizatsiya, widget extension, framework, entitlements va signing
📦 Bazel iOS — Mukammal Qo‘llanma & Amaliyot
Bazel 8: Haqiqiy ilova anatomiyasi — resurslar, lokalizatsiya, widget extension, framework, entitlements va signing
Holat: 2026-yil oktabr. Bu darsda bitta "o'yinchoq" SwiftUI ilovadan haqiqiy App Store ilovasiga o'tamiz: resurslar (asset catalog, JSON, shriftlar), 3 tilli lokalizatsiya,
apple_resource_bundlebilan namespaced resurslar,ios_frameworkorqali kodni app va widget orasida bo'lishish, WidgetKit widget extension, App Groups entitlements, versiyalash va code signing. Hamma kod Bazel 9.3.0, rules_apple 5.3.0, rules_swift 4.2.1, apple_support 2.10.1 va Xcode 27.1 bilan Apple M4 Pro'da (12 core, 24 GB) qurib va.ipaichi ochib tekshirilgan.
Bu darsdan keyin siz: bir nechta bundle (app + framework + extension), resurs va entitlements'dan iborat haqiqiy iOS ilovani Bazel'da to'liq qura olasiz va
.appichida nima qayerda turishini aniq bilasiz.
data(kutubxona resursi) bilan top-level atributlar (app_icons,infoplists,strings) orasidagi farqni va har biri bundle'da qayerga tushishini bilasiz.- Asset catalog, JSON,
.lprojlokalizatsiya vaapple_resource_bundleni to'g'ri joylashtirasiz va kod ularni runtime'da qanday topishini tushunasiz. ios_frameworkbilan Core kodini app va widget orasida bo'lishasiz;ios_extensionbilan WidgetKit widget qo'shasiz.- Entitlements (App Groups),
apple_bundle_version,select()bilan muhitga qarab sozlash va simulator vs qurilma signing'ni farqlaysiz.
Mundarija
- Nega "haqiqiy ilova" boshqacha
- [Resurslar:
datava top-level atributlar](#resurslar) - [Lokalizatsiya:
.lprojva.strings](#lokalizatsiya) - Resource bundle: namespaced resurslar
- Framework: kodni app va extension orasida bo'lishish
- [Widget extension: WidgetKit va
ios_extension](#widget) - Entitlements va App Groups
- Versiyalash va build konfiguratsiyalari
- Code signing: simulator va qurilma
- To'liq anatomiya va o'lchovlar
- Amaliy mashqlar
- O'zingizni tekshiring
- Xulosa
- Manbalar
1. Nega "haqiqiy ilova" boshqacha {#nega}
Bazel 5-darsda biz bitta swift_library + bitta ios_application dan iborat eng kichik SwiftUI ilovani qurdik. U kompilyatsiya bo'ldi, simulatorda ishga tushdi — lekin u haqiqiy App Store ilovasi emas edi. Haqiqiy ilovada doim quyidagilar bor:
- Resurslar: rasm va ranglar (asset catalog), shriftlar, JSON ma'lumotlar, ovozlar.
- Lokalizatsiya: kamida 2-3 til — ko'pincha
en,ru,uz. - Bir nechta bundle: asosiy app
.app, uning ichidaFrameworks/(umumiy kod) vaPlugIns/(extension'lar — widget, share extension, notification service...). - Entitlements: App Groups, push, keychain sharing kabi imkoniyatlar.
- Versiya va code signing:
CFBundleVersion, provisioning profile, sertifikat.
Xcode'da bularning hammasi project.pbxproj ichida — "Build Phases", "Copy Bundle Resources", "Embed App Extensions", "Signing & Capabilities" tablaridan tahrirlanadi. Jamoada o'sha bitta XML faylga hamma tegadi va merge conflict bo'laveradi. Bazel'da esa har bir bundle — alohida target, har bir resurs — target atributida aniq e'lon qilinadi. Katta iOS ilovalarida bu modullar soni yuzlab bo'lishi mumkin: Toss (Tuist case study, 2026-10-01) ~1000 ta modulga ega bo'lib, ularning aksariyati Interface/Implementation/Model/Testing target'lariga bo'lingan; Monzo esa ~50 dan ~200 modulgacha o'sgan (Tuist case study, 2026-05-26). Bunday miqyosda "qaysi resurs qaysi bundle'ga tushadi" degan savolga aniq, deklarativ javob bo'lishi shart.
🚀 Motivatsiya: Shu darsning oxirida sizning bitta
bazel build //App:Vaqtbuyrug'ingiz app binary'ni, uning ichidagiCore.frameworkni,VaqtWidget.appexwidget'ini, lokalizatsiyani (3 til +Base.lproj), asset catalog'ni va imzoni — hammasini bitta harakatda yasaydi. Hech qanday "Copy Bundle Resources" tabi, hech qanday qo'lda embedding yo'q. Buni.ipani ochib, har bir faylni ko'rsatib isbotlaymiz.
Bu darsda biz "Vaqt" nomli namoz vaqtlari ilovasini quramiz: u bundle'dagi JSON'dan kunlik jadvalni o'qiydi, 3 tilda ishlaydi, umumiy Core framework'ga ega va keyingi namozni ko'rsatadigan WidgetKit widget'i bor. Loyiha tuzilmasi:
VaqtApp/
├── MODULE.bazel # bazel_dep: apple_support, rules_swift, rules_apple
├── .bazelversion # 9.3.0
├── .bazelrc # disk cache + simulator flaglari
├── BUILD.bazel # apple_bundle_version (versiya)
├── Core/ # umumiy kutubxona + framework
│ ├── BUILD.bazel
│ ├── Info.plist
│ ├── Sources/ # model, store, lokalizatsiya yordamchisi
│ └── Resources/ # prayers.json
├── App/ # asosiy ilova
│ ├── BUILD.bazel
│ ├── Info.plist
│ ├── App.entitlements # App Group
│ ├── Sources/ # SwiftUI
│ └── Resources/ # Assets.xcassets + en/ru/uz/Base.lproj
└── Widget/ # WidgetKit extension
├── BUILD.bazel
├── Info.plist # NSExtensionPointIdentifier
├── Widget.entitlements
├── Sources/
└── Resources/
🏢 Real-world: iOS uchun ikki asosiy rule to'plami —
rules_applevarules_swift. Uchinchisi,rules_ios(bazel-ios tashkiloti), 2025-yil 7-noyabrda sunset qilindi: Square muhandisi jschear #952 muhokamada "rules_ios endi kamroq maqsadga xizmat qiladi va keraksiz murakkablik qo'shadi, chunki rules_apple va rules_swift kerakli funksiyalarni oldi" deb yozdi. Shuning uchun biz faqatrules_apple+rules_swiftishlatamiz — bu bugungi to'g'ri yo'l.
Boshlashdan oldin MODULE.bazel, .bazelversion va .bazelrc ni tayyorlab olamiz — bular 2-darsdan tanish:
# MODULE.bazel
module(name = "vaqt_app", version = "1.0.0")
bazel_dep(name = "apple_support", version = "2.10.1")
bazel_dep(name = "rules_swift", version = "4.2.1")
bazel_dep(name = "rules_apple", version = "5.3.0")
# .bazelrc
# Umumiy disk cache — jamoadagi barcha build'lar bitta cache'dan foydalanadi
build --disk_cache=~/.cache/bazel-disk
# Xcode 27.x: test runner default "iPhone 15" ni so'raydi va xato beradi;
# shuning uchun mavjud simulatorni ko'rsatamiz
test --ios_simulator_device="iPhone 17 Pro"
test --ios_simulator_version=27.0
🧠 Esda tuting:
rules_apple5.3.0bazel_compatibility = [">=8.0.0"]deb e'lon qiladi — ya'ni Bazel 8 va 9 bilan ishlaydi, Bazel 7 endi qo'llab-quvvatlanmaydi (5.0.0'da Bazel 7 olib tashlandi). Shuningdek 5.x da default iOS test runnerxctestrunrunner'iga o'tdi. Eski darsliklarda uchraydigan@build_bazel_rules_apple//...load yo'li endi kerak emas — oddiy@rules_apple//ishlaydi.
2. Resurslar: data va top-level atributlar {#resurslar}
Birinchi va eng muhim savol: resurs qayerga qo'yiladi va kod uni qanday topadi? rules_apple'ning rasmiy hujjati (resources.md) resurslarni ikki toifaga ajratadi:
- Kutubxona resurslari — kod runtime'da o'qiydigan fayllar (JSON, rasm, CoreData model). Bular
swift_library/objc_libraryning **data** atributi orqali e'lon qilinadi. - Top-level resurslar — operatsion tizim ishlatadigan fayllar (app ikonkasi, launch screen, lokalizatsiya). Bular top-level bundle rule'ning maxsus atributlari orqali:
app_icons,launch_storyboard,infoplists,strings.
Eng oddiy holat — JSON faylni data ga qo'shish. rules_apple hujjatidagi qoida: **kutubxonaga data orqali ulangan resurslar transitiv ravishda top-level bundle'ning ILDIZIGA qo'yiladi** (subkatalog yo'qoladi). Masalan:
# App/BUILD.bazel (soddalashtirilgan misol)
swift_library(
name = "AppLib",
srcs = glob(["Sources/**/*.swift"]),
data = ["Resources/prayers.json"], # → Vaqt.app/prayers.json
deps = ["//Core:CoreLib"],
)
Bu holatda Swift kod faylni Bundle.main orqali topadi:
guard let url = Bundle.main.url(forResource: "prayers", withExtension: "json") else {
throw PrayerStoreError.resourceNotFound("prayers.json")
}
let data = try Data(contentsOf: url)
⚠️ Diqqat: rules_swift **SwiftPM'dagi
Bundle.moduleni avtomatik yaratmaydi.** SwiftPM'daBundle.modulekutubxonaning o'z resurs bundle'iga ishora qiladi; Bazel'da esadataresurslari standart holatda app ildiziga tushadi vaBundle.mainorqali topiladi. Agar kodingizdaBundle.moduleyozsangiz, kompilyatsiya xatosi olasiz. Buning yechimi — keyingi bo'limdagiapple_resource_bundle(namespaced bundle, framework ichida ham ishonchli ishlaydi).
Asset catalog (.xcassets) — eng keng tarqalgan resurs. Unda rang (colorset), ikonka (imageset), app icon (appiconset) bo'ladi. Biz ilovaga ikkita rang qo'shamiz. Avval asset catalog fayllarini yaratamiz:
// App/Resources/Assets.xcassets/Accent.colorset/Contents.json
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"red": "0.102",
"green": "0.600",
"blue": "0.412",
"alpha": "1.000"
}
},
"idiom": "universal"
}
],
"info": { "author": "xcode", "version": 1 }
}
Asset catalog .xcassets papkasi butunligicha data ga glob() bilan qo'shiladi (ichida har bir Contents.json bor):
# App/BUILD.bazel
swift_library(
name = "AppLib",
srcs = glob(["Sources/**/*.swift"]),
module_name = "VaqtApp",
# Asset catalog va lokalizatsiya — kutubxona darajasida data orqali
data = glob([
"Resources/Assets.xcassets/**",
"Resources/**/*.lproj/**",
]),
deps = ["//Core:CoreLib"],
)
SwiftUI kodida rangni nomi bilan ishlatamiz — rules_apple build vaqtida actool ni chaqirib .xcassets ni Assets.car ga kompilyatsiya qiladi:
// App/Sources/ContentView.swift (qism)
.background(Color("Accent").opacity(0.15))
Build'dan keyin .app ichida Assets.car paydo bo'lishini unzip -l bilan ko'ramiz (to'liq ro'yxatni 10-bo'limda keltiramiz):
Payload/Vaqt.app/Assets.car ← .xcassets → actool → Assets.car
Payload/Vaqt.app/prayers.json ← data (agar app darajasida bo'lsa, ildizda)
💡 Pro tip:
app_iconsatributidatadan farq qiladi — unga faqat bitta*.xcassets/*.appiconsetkatalogini berasiz. Xcode 26+ uchunapp_iconsyangi Icon Composer*.iconbundle'ini ham qabul qiladi (26 va undan oldingi OS'lar uchun yonma-yon). App icon — top-level resurs, chunki uni OS ishlatadi, kod emas.
3. Lokalizatsiya: .lproj va .strings {#lokalizatsiya}
iOS lokalizatsiyasi .lproj papkalariga asoslanadi: en.lproj/, ru.lproj/, uz.lproj/, va Base.lproj/ (zaxira). Har birida bir xil nomli Localizable.strings fayl bo'ladi — faqat qiymatlar har xil tilda. Bizning ilova uchun uchta til yozamiz:
# App/Resources/en.lproj/Localizable.strings
"next_prayer" = "Next prayer";
"app_title" = "Prayer Times";
# App/Resources/ru.lproj/Localizable.strings
"next_prayer" = "Следующая молитва";
"app_title" = "Время молитв";
# App/Resources/uz.lproj/Localizable.strings
"next_prayer" = "Keyingi namoz";
"app_title" = "Namoz vaqtlari";
.lproj papkalari — kutubxona resurslari (OS ularni foydalanuvchi tiliga qarab o'qiydi), shuning uchun ularni data orqali glob() bilan qo'shamiz. Muhim: glob ichida *.lproj/** pattern'ini ishlating, aks holda papka ichidagi fayllar tushib qolmaydi:
# App/BUILD.bazel (data qismi)
data = glob([
"Resources/Assets.xcassets/**",
"Resources/**/*.lproj/**", # en/ru/uz/Base.lproj — hammasi
]),
Swift tomonda standart holat — NSLocalizedString, u foydalanuvchi tilini Bundle.main orqali tanlaydi. Lekin biz darsda har uchala tilni bir vaqtda ko'rsatmoqchimiz (resurslar haqiqatan ham bundle'ga tushganini isbotlash uchun), shuning uchun kerakli .lproj ni aniq ochamiz:
// Core/Sources/Localized.swift
import Foundation
// Lokalizatsiya kalitini berilgan til kodida qaytaradi.
// lang berilmasa — tizim tili ishlatiladi (Bundle.main).
public func localizedTitle(_ key: String, lang: String? = nil) -> String {
guard let lang,
let path = Bundle.main.path(forResource: lang, ofType: "lproj"),
let bundle = Bundle(path: path) else {
return NSLocalizedString(key, bundle: .main, comment: "")
}
return bundle.localizedString(forKey: key, value: key, table: nil)
}
Ilovani simulatorda ishga tushirganimizda (bazel run //App:Vaqt), ekranning pastki qismida uchala til ham to'g'ri chiqdi — demak uch .lproj ham bundle'da bor va runtime'da topilmoqda:
Vaqt ilovasi simulatorda: Toshkent shahri, 5 namoz vaqti JSON'dan o'qilgan, keyingi namoz (Asr 15:47) yashil "Accent" rangda, pastda uchala tilda "Keyingi namoz / Следующая молитва / Next prayer"
Yuqoridagi skrinshotda uchta mexanizm bir vaqtda ishlayapti: (1) prayers.json Core framework'dan o'qildi — shahar "Toshkent", 5 ta vaqt; (2) asset catalog'dagi "Accent" yashil rang keyingi namozni (Asr 15:47) ajratib ko'rsatyapti; (3) uchala til .lproj papkasidan yechildi.
Build'dan keyin .ipa ichida barcha .lproj papkalari app ildizida turadi — biz buni unzip -l bilan ko'rdik:
Payload/Vaqt.app/Base.lproj/Localizable.strings
Payload/Vaqt.app/en.lproj/Localizable.strings
Payload/Vaqt.app/ru.lproj/Localizable.strings
Payload/Vaqt.app/uz.lproj/Localizable.strings
Katta ilovalarda har bir qo'shimcha til .app hajmini oshiradi, ayniqsa framework'lar o'z tarjimalarini olib kelsa. rules_apple buni boshqarishga ikki yo'l beradi (common_info.md):
- Build vaqtida cheklash — faqat kerakli tillarni qadoqlash (debug/test tsiklini tezlashtiradi, chunki aksariyat dasturchi bitta tilda ishlaydi):
# Faqat o'zbek tilini qadoqlash (qolgan .lproj'lar o'tkazib yuboriladi)
bazel build //App:Vaqt --define "apple.locales_to_include=uz"
- Target darajasida —
locales_to_includeatributi (bu--definedan ustun turadi):
ios_application(
name = "Vaqt",
locales_to_include = ["uz", "ru"],
# ...
)
- Avtomatik tozalash —
--define "apple.trim_lproj_locales=yes"ilova qo'llab-quvvatlamaydigan, lekin framework'lar olib kelgan ortiqcha.lprojpapkalarini olib tashlaydi.
🧠 Esda tuting:
Base.lprojhar doim qo'shiladi, hattolocales_to_includeda ko'rsatilmasa ham. Til nomlari aniq moslashtiriladi:ptyetarli emas, kerak bo'lishi mumkinpt_BRyokipt_PT— chunki papka nomi aynan shunday. Bu--defineda ham,locales_to_includeatributida ham amal qiladi.
⚠️ Diqqat:
apple.locales_to_include— bu--define, ya'ni u action key'ga ta'sir qiladi (10-darsda cache haqida batafsil). Ya'ni--define "apple.locales_to_include=uz"bilan qurilgan bundle va to'liq bundle — ikki xil cache yozuvi. Development uchun qulay, lekin release build'da to'liq to'plamni ishlating.
4. Resource bundle: namespaced resurslar {#resource-bundle}
2-bo'limda ko'rdikki, data orqali qo'shilgan resurs app ildiziga tushadi. Bu kichik ilovaga yaxshi, lekin umumiy framework uchun muammo: agar Core framework o'zining config.json iga ega bo'lsa va ilovada ham config.json bo'lsa — ular app ildizida to'qnashadi. Yechim — resurslarni namespaced bundle ichiga joylash. rules_apple hujjati aynan shuni tavsiya qiladi: "umumiy kutubxonalar resurslarini bundle ichida namespace qilgani qulay, shunda client ilovasi resurslari bilan to'qnashmaydi".
Buning uchun apple_resource_bundle ishlatamiz. Bizning prayers.json — Core kutubxonasiga tegishli, shuning uchun uni CoreResources.bundle ga joylaymiz:
# Core/BUILD.bazel
load("@rules_swift//swift:swift.bzl", "swift_library")
load("@rules_apple//apple:resources.bzl", "apple_resource_bundle")
swift_library(
name = "CoreLib",
srcs = glob(["Sources/**/*.swift"]),
module_name = "Core",
data = [":CoreResources"], # namespaced bundle'ni data orqali ulaymiz
visibility = ["//visibility:public"],
)
# Namespaced resource bundle — prayers.json shu yerda yashaydi
apple_resource_bundle(
name = "CoreResources",
bundle_id = "uz.azimjondev.vaqt.core.resources",
resources = ["Resources/prayers.json"],
)
Endi Swift kodda resursni topish o'zgaradi. prayers.json CoreResources.bundle ichida, u esa Core framework ichida. Kutubxona kodidan framework bundle'iga yetib olish uchun Bundle(for:) ga "langar" (anchor) klass kerak bo'ladi — chunki struct da klass yo'q:
// Core/Sources/PrayerStore.swift (qism)
import Foundation
// Core framework ichidagi bundle'ni topish uchun "langar" klass.
final class CoreBundleToken {}
public struct PrayerStore: Sendable {
public init() {}
public func resourcesBundle() throws -> Bundle {
// Core framework'ning o'z bundle'i (Bundle(for:) orqali)
let coreBundle = Bundle(for: CoreBundleToken.self)
// CoreResources.bundle avval framework ichida, bo'lmasa app root'ida qidiramiz
for base in [coreBundle, Bundle.main] {
if let url = base.url(forResource: "CoreResources", withExtension: "bundle"),
let bundle = Bundle(url: url) {
return bundle
}
}
throw PrayerStoreError.bundleNotFound
}
public func loadToday() throws -> DaySchedule {
let bundle = try resourcesBundle()
guard let url = bundle.url(forResource: "prayers", withExtension: "json") else {
throw PrayerStoreError.resourceNotFound("prayers.json")
}
let data = try Data(contentsOf: url)
return try JSONDecoder().decode(DaySchedule.self, from: data)
}
}
Build qilib .ipa ni ochganimizda — CoreResources.bundle aynan Core framework ICHIDA, app ildizida emas (bu tekshirilgan haqiqiy natija):
Payload/Vaqt.app/Frameworks/Core.framework/CoreResources.bundle/prayers.json
🧠 Esda tuting: Resurs egasi qayerda bo'lsa — bundle o'sha yerda.
CoreResourcesniCoreLibningdatasiga uladik;CoreLibesaCoreframework'ga kiradi. Shuning uchunCoreResources.bundleframework ichida joylashdi. Agar xuddi shu bundle'ni to'g'ridan-to'g'ri app kutubxonasiga ulasangiz, u app ildizida bo'lardi. Bu — "resurs qayerga tushadi" diagrammasining asosiy g'oyasi:
Resurs e'lon qilish usuli (chapda) va uning bundle'dagi joyi (o'ngda): data → app ildizi; apple_resource_bundle → namespaced .bundle; apple_resource_group structured_resources → katalog saqlanadi; top-level atributlar → Assets.car/Info.plist/.lproj
rules_apple'da resurs bundle yaratishning uch yo'li bor (resources.md):
| Rule | Nima qiladi | Qachon ishlatiladi |
|---|---|---|
apple_resource_bundle | Bir nechta fayldan .bundle quradi | Umumiy framework resurslari (bizning holat) |
apple_bundle_import | Allaqachon tayyor .bundle papkasini import qiladi | Checked-in tayyor bundle |
apple_resource_group | Resurslarni guruhlaydi, structured_resources bilan katalog strukturasini saqlaydi | Subkatalog muhim bo'lsa (masalan audio/azon.mp3) |
apple_resource_group ning asosiy farqi — resources va structured_resources atributlari. resources fayllarni ildizga tekis yoyadi (subkatalog yo'qoladi), structured_resources esa katalog strukturasini saqlaydi. rules_apple hujjatidagi misol:
apple_resource_group(
name = "MyResources",
resources = ["MyText.txt"], # → App.app/MyText.txt
structured_resources = ["Subdirectory/x.plist"], # → App.app/Subdirectory/x.plist
)
💡 Pro tip: JSON,
.strings,.pngkabi "qayta ishlanadigan" fayllar bundling vaqtida optimallashtiriladi (masalan.pngsiqiladi,.stringsbinary plist'ga aylanadi).structured_resourcesesa hech qanday qayta ishlanmaydi — fayl qanday bo'lsa, shundayligicha ko'chiriladi. Agar fayl aynan o'zgarmasligi kerak bo'lsa (masalan oldindan hisoblangan ma'lumot),structured_resourcesishlating.
5. Framework: kodni app va extension orasida bo'lishish {#framework}
Bizning Core modulida PrayerTime, DaySchedule, PrayerStore va localizedTitle bor. Buni ham ilova, ham widget ishlatadi. Agar Core'ni oddiy swift_library sifatida qoldirsak, uning kodi ikki marta — bir marta app binary'ga, bir marta widget binary'ga — statik ravishda link qilinadi. Natijada bir xil kod ikki nusxada, bundle kattalashadi va Core resurslari (prayers.json) ham ikki joyda bo'ladi.
Yechim — Coreni dinamik framework qilish. Shunda Core.framework app ichidagi Frameworks/ papkasiga bir marta joylashadi, app ham, widget ham uni @rpath orqali bog'laydi:
# Core/BUILD.bazel (framework qismi)
load("@rules_apple//apple:ios.bzl", "ios_framework")
ios_framework(
name = "Core",
bundle_id = "uz.azimjondev.vaqt.core",
extension_safe = True, # faqat extension-xavfsiz API'lar
families = ["iphone"],
infoplists = ["Info.plist"],
minimum_os_version = "17.0",
deps = [":CoreLib"],
visibility = ["//visibility:public"],
)
ios_framework ning muhim atributi — **extension_safe = True**. U framework'ni -application-extension flagı bilan kompilyatsiya qiladi, ya'ni faqat extension'larda ruxsat etilgan API'lardan foydalanishni majburlaydi. Agar framework'ni extension ishlatsa (bizda shunday — widget), bu bayroq shart: aks holda widget ichida UIApplication.shared kabi extension'da taqiqlangan API ishlatib qo'ysangiz, kompilyator ogohlantiradi.
App va widget framework'ni frameworks atributi orqali oladi (deps emas — bu muhim farq):
# App/BUILD.bazel
ios_application(
name = "Vaqt",
frameworks = ["//Core"], # Core.framework → Vaqt.app/Frameworks/
# ...
deps = [":AppLib"],
)
# Widget/BUILD.bazel
ios_extension(
name = "VaqtWidget",
frameworks = ["//Core"], # xuddi shu framework'ni ulaydi
# ...
deps = [":WidgetLib"],
)
Build'dan keyin otool -L bilan tekshirsak, widget binary'si aynan @rpath/Core.framework/Core ga bog'langanini ko'ramiz — ya'ni u app bilan bir xil framework nusxasini ishlatadi (tekshirilgan haqiqiy natija):
otool -L Payload/Vaqt.app/PlugIns/VaqtWidget.appex/VaqtWidget | grep Core
@rpath/Core.framework/Core (compatibility version 0.0.0, current version 0.0.0)
🧠 Esda tuting:
deps— kodni bu target'ning binary'siga link qiladi (statik).frameworks— alohida dinamik framework bundle'ini ichiga joylashtiradi va unga bog'laydi. Umumiy kodni app + extension orasida bo'lishmoqchi bo'lsangiz —ios_framework+frameworks. Aks holda kod har bir binary'ga nusxalanadi.
💡 Pro tip: Hamma umumiy kodni ham dinamik framework qilish shart emas. Dinamik framework ishga tushganda yuklanadi (startup vaqtiga ta'sir qiladi). Agar kod faqat bitta bundle'da ishlatilsa, oddiy
swift_library(statik) tezroq. Dinamik framework'ni bir nechta bundle bo'lishganda (app + extension, yoki app + app clip) ishlating. Statik framework kerak bo'lsa —ios_static_frameworkbor.
6. Widget extension: WidgetKit va ios_extension {#widget}
App extension — bu ilova ichida yashaydigan alohida bajariladigan modul: widget, Share extension, Notification Service, Shortcuts... Ularning hammasi .appex bundle sifatida app ichidagi PlugIns/ papkasiga joylashadi. Biz keyingi namoz vaqtini ko'rsatadigan WidgetKit widget quramiz.
WidgetKit widget'ining ikki o'ziga xosligi bor:
- U
@mainorqali ishga tushadi (an'anaviymainkirish nuqtasi) — ko'pchilik boshqa extension'lar esa "plug-in" modelida ishlaydi, kirish nuqtasini system framework beradi. - Uning
Info.plistidaNSExtensionPointIdentifier = com.apple.widgetkit-extensionbo'lishi shart.
Avval widget kodini yozamiz. U Core dagi PrayerStore ni ishlatadi — framework widget ichida ham borligi uchun bir xil store ishlaydi:
// Widget/Sources/VaqtWidget.swift
import WidgetKit
import SwiftUI
import Core
struct PrayerEntry: TimelineEntry {
let date: Date
let prayerName: String
let prayerTime: String
}
struct Provider: TimelineProvider {
private func current() -> PrayerEntry {
guard let schedule = try? PrayerStore().loadToday() else {
return PrayerEntry(date: Date(), prayerName: "—", prayerTime: "--:--")
}
let next = PrayerStore().nextPrayer(in: schedule, nowMinutes: 13 * 60)
return PrayerEntry(date: Date(), prayerName: next.name, prayerTime: next.time)
}
func placeholder(in context: Context) -> PrayerEntry {
PrayerEntry(date: Date(), prayerName: "Peshin", prayerTime: "12:28")
}
func getSnapshot(in context: Context, completion: @escaping (PrayerEntry) -> Void) {
completion(current())
}
func getTimeline(in context: Context, completion: @escaping (Timeline<PrayerEntry>) -> Void) {
completion(Timeline(entries: [current()], policy: .atEnd))
}
}
struct VaqtWidgetView: View {
let entry: PrayerEntry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(localizedTitle("next_prayer", lang: "uz"))
.font(.caption2).foregroundStyle(.secondary)
Text(entry.prayerName).font(.headline)
Text(entry.prayerTime).font(.title2.monospacedDigit().bold())
.foregroundStyle(Color("Accent"))
}
.containerBackground(.fill.tertiary, for: .widget)
}
}
@main
struct VaqtWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "VaqtWidget", provider: Provider()) { entry in
VaqtWidgetView(entry: entry)
}
.configurationDisplayName("Namoz vaqti")
.description("Keyingi namoz vaqtini ko'rsatadi")
.supportedFamilies([.systemSmall])
}
}
Widget'ning Info.plist i — bu yerda NSExtensionPointIdentifier kalit WidgetKit'ni bildiradi:
# Widget/Info.plist (asosiy qism)
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
Endi ios_extension bilan widget'ni quramiz va uni app ichiga joylaymiz:
# Widget/BUILD.bazel
load("@rules_swift//swift:swift.bzl", "swift_library")
load("@rules_apple//apple:ios.bzl", "ios_extension")
swift_library(
name = "WidgetLib",
srcs = glob(["Sources/**/*.swift"]),
module_name = "VaqtWidgetExt",
data = glob(["Resources/Assets.xcassets/**"]), # widget ham "Accent" rangni ishlatadi
deps = ["//Core:CoreLib"],
)
ios_extension(
name = "VaqtWidget",
bundle_id = "uz.azimjondev.vaqt.widget",
families = ["iphone"],
frameworks = ["//Core"],
entitlements = "Widget.entitlements",
infoplists = ["Info.plist"],
minimum_os_version = "17.0",
version = "//:AppVersion",
visibility = ["//App:__pkg__"], # ↙ buni qo'shmaguncha xato oldik (pastda)
deps = [":WidgetLib"],
)
# App/BUILD.bazel
ios_application(
name = "Vaqt",
extensions = ["//Widget:VaqtWidget"], # widget'ni app ichiga joylaymiz
# ...
)
Haqiqiy xato: visibility
Birinchi urinishda Widget/BUILD.bazel da visibility ni yozmagan edim. bazel build //App:Vaqt darhol to'xtadi:
ERROR: .../App/BUILD.bazel:16:16: in ios_application rule //App:Vaqt: Visibility error:
target '//Widget:VaqtWidget' is not visible from target '//App:Vaqt'
Sabab — 3-darsdan bilamiz: target'lar standart holatda private (faqat o'z paketiga ko'rinadi). App paketi Widget paketidagi target'ni ko'ra olmaydi. Yechim — widget'ga aniq visibility berish. Men ["//App:__pkg__"] ni tanladim (faqat App paketiga ko'rinadi) — chunki widget faqat shu app'ga tegishli, uni butun repo'ga ochish shart emas. Bu — visibility'ni arxitektura chegarasi sifatida ishlatish.
Tuzatgandan so'ng build o'tdi va .app ichida PlugIns/VaqtWidget.appex paydo bo'ldi (tekshirilgan):
Payload/Vaqt.app/PlugIns/VaqtWidget.appex/VaqtWidget ← widget binary
Payload/Vaqt.app/PlugIns/VaqtWidget.appex/Assets.car ← widget o'z asset catalog'i
Payload/Vaqt.app/PlugIns/VaqtWidget.appex/Info.plist ← NSExtensionPointIdentifier
Payload/Vaqt.app/PlugIns/VaqtWidget.appex/_CodeSignature/ ← alohida imzolangan
🧠 Esda tuting: WidgetKit widget
@mainishlatgani uchunNSExtensionPrincipalClasskerak emas. Lekin plug-in asosidagi extension'larda (Share, Notification Service)Info.plistdaNSExtensionPrincipalClass = $(PRODUCT_MODULE_NAME).MyClassyoziladi. Diqqat: rules_apple **PRODUCT_MODULE_NAMEni almashtirmaydi** (common_info.mdbuni aniq aytadi), chunki Bazel'da har bir target alohida modul. Shuning uchun plug-in extension'da modul nomini qo'lda yozishingiz kerak, masalanVaqtWidgetExt.MyClass.
⚠️ Diqqat:
ios_extensiondaextensionkit_extensiondegan atribut bor (defaultFalse). U ExtensionKit (iOS 16+ yangi model) extension'lari uchun. WidgetKit widget'lari ExtensionKit emas — ular an'anaviyNSExtensionplug-in modelida,@mainkirish nuqtasi bilan ishlaydi. Shuning uchun bizextensionkit_extensionni o'zgartirmaymiz (Falseqoldiramiz). Buni noto'g'riTrueqilsangiz, widget ishga tushmaydi.
Build grafigi endi quyidagicha ko'rinadi — bazel query 'rdeps(//..., //Core:CoreLib)' natijasi Core'ga bog'liq hamma target'ni ko'rsatadi:
//App:AppLib //App:Vaqt
//Core:CoreLib //Core:Core
//Widget:WidgetLib //Widget:VaqtWidget
Ya'ni Core — grafning markazi: uni app ham, widget ham ishlatadi. Keyingi bo'limda bu umumiylik entitlements orqali ma'lumot almashishga qanday yordam berishini ko'ramiz.
7. Entitlements va App Groups {#entitlements}
Entitlements — bu ilovaga maxsus imkoniyatlar beradigan ruxsatlar ro'yxati: App Groups, Push Notifications, Keychain Sharing, iCloud va h.k. Ular .entitlements (plist) faylida yoziladi va bundle rule'ning entitlements atributi orqali ulanadi.
Bizning widget app bilan ma'lumot almashishi kerak (masalan, foydalanuvchi tanlagan shahar). Buning standart yo'li — App Group: app ham, widget ham bir xil guruhga a'zo bo'ladi va umumiy UserDefaults(suiteName:) yoki umumiy fayl konteyneridan foydalanadi. Buning uchun ikkala bundle ham bir xil App Group entitlement'iga ega bo'lishi shart:
# App/App.entitlements
<key>com.apple.security.application-groups</key>
<array>
<string>group.uz.azimjondev.vaqt</string>
</array>
# Widget/Widget.entitlements — AYNAN bir xil guruh
<key>com.apple.security.application-groups</key>
<array>
<string>group.uz.azimjondev.vaqt</string>
</array>
Ularni app va widget rule'lariga ulaymiz:
ios_application(
name = "Vaqt",
entitlements = "App.entitlements",
# ...
)
ios_extension(
name = "VaqtWidget",
entitlements = "Widget.entitlements",
# ...
)
Swift tomonda umumiy ma'lumot shunday yoziladi/o'qiladi (ikkala bundle'da bir xil suite nomi):
let shared = UserDefaults(suiteName: "group.uz.azimjondev.vaqt")
shared?.set("Toshkent", forKey: "selectedCity") // app yozadi
let city = shared?.string(forKey: "selectedCity") // widget o'qiydi
Entitlement qayerga yoziladi: simulator va qurilma farqi
Bu yerda muhim va chalg'ituvchi nuqta bor. Build qilib app binary'ning imzosini codesign bilan ko'rsak, App Group entitlement'ini ko'rmaymiz — faqat get-task-allow chiqadi:
codesign -dv --entitlements - --xml Payload/Vaqt.app
CodeDirectory v=20400 size=651 flags=0x2(adhoc) ...
Signature=adhoc
TeamIdentifier=not set
<plist><dict><key>com.apple.security.get-task-allow</key><true/></dict></plist>
App Group qayerga ketdi? common_info.md javob beradi: simulator build'ida entitlement'lar Mach-O segmentiga qo'shiladi, imzo blokiga emas. Buni otool bilan tekshiramiz — mana haqiqiy natija (__TEXT,__entitlements segmenti dekodlangan):
otool -s __TEXT __entitlements Payload/Vaqt.app/Vaqt
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.uz.azimjondev.vaqt</string>
</array>
<key>get-task-allow</key>
<true/>
</dict>
</plist>
Xuddi shu entitlement widget binary'sida ham bor — ikkalasi bir App Group'da. Demak rules_apple App Group'ni to'g'ri qo'ydi, shunchaki simulator uchun uni imzo blokiga emas, Mach-O segmentiga yozdi.
🧠 Esda tuting: Simulator bundle'ida entitlement'ni tekshirish uchun
codesign -d --entitlementsemas,otool -s __TEXT __entitlementsishlating — birinchisi (adhoc) imzo blokini ko'rsatadi, u yerda faqatget-task-allowbor; haqiqiy entitlement'lar Mach-O segmentida.
⚠️ Diqqat: App Group'ni simulatorda o'qish ishlaydi, lekin ikki process orasida (app ↔ widget) yozib bo'lishish ba'zan faqat bundle imzolangan bo'lsa ishlaydi.
common_info.mdaytadi: "Shared App Groups kabi ba'zi entitlement'lar uchun simulator bundle imzolangan bo'lishini talab qiladi". rules_apple default holatda simulator main bundle'ini adhoc imzolaydi, shuning uchun bu odatda ishlaydi — lekin App Group orqali ishonchli cross-process yozishni biz jonli qurilmada tekshirishni tavsiya qilamiz (bizda Apple hisobi/qurilma yo'q, shuning uchun bu qismni hujjat asosida aytyapmiz).
💡 Pro tip: Bizning darsdagi widget App Group'ga tayanmaydi — u
Core.frameworkichidagiprayers.jsonni o'qiydi. Bu ataylab: bundle'dagi resurs deterministik va simulatorda ishonchli ishlaydi, App Group esa yoziladigan umumiy holat uchun. Agar widget faqat o'qiydigan ma'lumotga muhtoj bo'lsa — umumiy framework resursidan foydalaning, App Group'ga hojat yo'q.
8. Versiyalash va build konfiguratsiyalari {#versiya}
Har bir App Store ilovasida ikki versiya raqami bor: CFBundleShortVersionString (foydalanuvchi ko'radigan "1.0") va CFBundleVersion (build raqami "1.0.3"). Bazel'da bularni apple_bundle_version target orqali boshqaramiz — Info.plist'da qo'lda yozish shart emas:
# BUILD.bazel (workspace root)
load("@rules_apple//apple:versioning.bzl", "apple_bundle_version")
apple_bundle_version(
name = "AppVersion",
build_version = "1.0.3", # → CFBundleVersion
short_version_string = "1.0", # → CFBundleShortVersionString
visibility = ["//visibility:public"],
)
Uni app ham, widget ham version atributi orqali oladi:
ios_application(name = "Vaqt", version = "//:AppVersion", ...)
ios_extension( name = "VaqtWidget", version = "//:AppVersion", ...)
Build'dan keyin ikkala bundle'da ham versiya bir xil bo'lishini tekshirdik (haqiqiy natija):
Vaqt.app/Info.plist → CFBundleShortVersionString=1.0, CFBundleVersion=1.0.3
PlugIns/VaqtWidget.appex → CFBundleVersion=1.0.3
🧠 Esda tuting: Extension versiyasi app versiyasi bilan mos kelishi kerak — aks holda App Store yuklashni rad etadi. Shuning uchun bitta
apple_bundle_versiontarget'ni ikkala bundle ham ishlatgani qulay: versiyani bir joyda o'zgartirasiz, hamma bundle yangilanadi.
CI uchun: --embed_label bilan versiya
Haqiqiy loyihalarda build raqamini CI beradi (masalan git tag yoki pipeline raqami). apple_bundle_version buni build_label_pattern + --embed_label orqali qo'llab-quvvatlaydi (hujjatdagi misol):
apple_bundle_version(
name = "build_label_version",
build_label_pattern = "Vaqt_{version}_build_{build}",
build_version = "{version}.{build}",
capture_groups = {
"version": "\\d+\\.\\d+",
"build": "\\d+",
},
short_version_string = "{version}",
fallback_build_label = "Vaqt_99.99_build_99", # dasturchi build'i uchun
)
# CI shunday chaqiradi:
bazel build //App:Vaqt --embed_label=Vaqt_1.2_build_345
# → CFBundleVersion = 1.2.345, CFBundleShortVersionString = 1.2
# --embed_label'siz (dasturchi) → fallback: 99.99.99
select() bilan muhitga qarab sozlash
Haqiqiy ilovada "dev" va "prod" muhitlari bo'ladi: har xil bundle_id, API manzili yoki ilova nomi. 4-darsdan bilgan select() + config_setting bu yerda ishga tushadi. Masalan, debug va release build'da ilova nomini farqlash — har xil Info.plist fragmentlari bilan:
# App/BUILD.bazel
config_setting(
name = "release_build",
values = {"compilation_mode": "opt"},
)
ios_application(
name = "Vaqt",
# Info.plist'lar birlashtiriladi; select() muhitga qarab fragment tanlaydi
infoplists = ["Info.plist"] + select({
":release_build": ["Info.release.plist"], # CFBundleDisplayName = "Vaqt"
"//conditions:default": ["Info.debug.plist"], # CFBundleDisplayName = "Vaqt DEV"
}),
# ...
)
rules_apple bir nechta Info.plist faylni birlashtiradi (common_info.md): agar ikki faylda bir xil kalit bo'lsa, qiymatlari bir xil bo'lishi kerak; aks holda build xato beradi. Shuning uchun Info.release.plist ichida faqat farq qiladigan kalitlarni (masalan CFBundleDisplayName) qo'ying.
Xuddi shunday, kodga muhit qiymatlarini defines orqali select() bilan uzatish mumkin:
swift_library(
name = "AppLib",
defines = select({
":release_build": ["API_BASE_URL=\\\"https://api.vaqt.uz\\\""],
"//conditions:default": ["API_BASE_URL=\\\"https://dev.vaqt.uz\\\""],
}),
# ...
)
🧠 Esda tuting: Info.plist'da Xcode uslubidagi o'zgaruvchilar ishlaydi:
$(PRODUCT_NAME),$(PRODUCT_BUNDLE_IDENTIFIER),$(EXECUTABLE_NAME). Biz app Info.plist'ida$(PRODUCT_NAME)yozdik — build'da uVaqtga aylandi (tekshirilgan). Lekin$(PRODUCT_MODULE_NAME)qo'llab-quvvatlanmaydi — uni qo'lda yozing.
⚠️ Diqqat:
select()konfiguratsiya action key'ning bir qismi. Ya'ni-c optva-c dbgbuild'lari har xil cache yozuvlariga ega — bu to'g'ri va kerakli xatti-harakat, lekin cache hajmini oshiradi. Buni 10-darsda (cache) batafsil ko'ramiz.
9. Code signing: simulator va qurilma {#signing}
Code signing — bu bundle'ni kriptografik imzolash: iOS faqat ishonchli imzoga ega ilovalarni ishga tushiradi. Bazel'da signing Xcode'ning "Automatically manage signing" dan boshqacha ishlaydi — hamma narsa aniq, deklarativ.
Simulator (biz tekshirgan yo'l)
Simulator entitlements'ni to'liq tekshirmaydi, shuning uchun:
provisioning_profileshart emas — Apple hisobi ham, sertifikat ham kerak emas.- rules_apple bundle'ni adhoc imzolaydi (
Signature=adhoc,TeamIdentifier=not set). bazel run //App:Vaqtilovani darhol simulatorga o'rnatib ishga tushiradi.
Bizning butun darsdagi build va run aynan shu yo'ldan ketdi — hech qanday hisob talab qilinmadi. Bu — o'rganish va CI testlari uchun ideal.
Qurilma (hujjat asosida — bizda qurilma/hisob yo'q)
Jonli qurilmaga chiqarish uchun signing majburiy. rules_apple'da (claim: rules_xcodeproj FAQ + rules-apple.md):
- Top-level target'lar
provisioning_profileatributini oladi, odatdalocal_provisioning_profileorqali to'ldiriladi:
load("@rules_apple//apple:apple.bzl", "local_provisioning_profile")
local_provisioning_profile(
name = "vaqt_profile",
profile_name = "Vaqt Development",
team_id = "ABC123DEF4", # bir nechta hisob orasida ajratish uchun
)
ios_application(
name = "Vaqt",
provisioning_profile = ":vaqt_profile",
# ...
)
local_provisioning_profileprofilni mashinangizdagi~/Library/MobileDevice/Provisioning Profilesdan topadi (bir nechta mos profil bo'lsa — eng yangisini tanlaydi). Profil o'rnatilmagan bo'lsa, Xcode "couldn't find any provisioning profiles matching 'TEAM_ID/PROFILE_NAME'" deydi.- Sertifikat (signing identity)
--@rules_apple//apple/build_settings:signing_certificate_namebilan tanlanadi (common_info.md):
# .bazelrc
build --@rules_apple//apple/build_settings:signing_certificate_name="Apple Development: Ism Familiya"
- rules_xcodeproj bilan Xcode-managed profilni ishlatish uchun uni
xcode_provisioning_profile(managed_by_xcode = True, ...)ichiga o'rab,team_idni berish kerak. Buni 9-darsda ko'ramiz.
⚠️ Diqqat: Yuqoridagi qurilma signing qismini biz shu mashinada tekshira olmadik — buning uchun Apple Developer hisobi va jonli qurilma kerak. Kod va flaglar rasmiy hujjatlardan olingan. Simulator yo'li esa to'liq tekshirilgan.
Signing diagrammasi
Code signing ikki yo'li: chapda simulator (tekshirilgan) — provisioning profile shart emas, adhoc imzo, entitlement Mach-O segmentida; o'ngda qurilma (hujjat asosida) — provisioning profile majburiy, signing identity, entitlement imzo blokida, CI'da profil+keychain oldindan
CI va remote cache: signing'ni lokal qoldirish
Jamoada remote cache ishlatilsa (10-11 darslar), signing va bundling action'larini remote'da bajarmaslik tavsiya etiladi — chunki katta bundle va bog'langan binary'larni yuklab-tushirish lokalda bajarishdan sekinroq. rules_apple buning uchun tayyor --modify_execution_info qatorini beradi (common_info.md):
# .bazelrc (remote cache ishlatganda)
common --modify_execution_info=^(BundleApp|BundleTreeApp|DsymDwarf|DsymLipo|GenerateAppleSymbolsFile|ObjcBinarySymbolStrip|CppArchive|CppLink|ObjcLink|ProcessAndSign|SignBinary|SwiftArchive|SwiftStdlibCopy)$=+no-remote,^(BundleResources|ImportedDynamicFrameworkProcessor)$=+no-remote-exec
Sabab: ProcessAndSign va SignBinary action'lari RBE (remote execution) uchun odatda sozlanmagan; BundleApp esa katta bundle yaratadi — uni yuklash samarasiz.
10. To'liq anatomiya va o'lchovlar {#anatomiya}
Endi hammasini birlashtiramiz. bazel build //App:Vaqt bitta buyruq bilan 398 ta action bajarib, quyidagi to'liq .app ni yasadi (bu — unzip -l Vaqt.ipa ning haqiqiy natijasi):
Payload/Vaqt.app/
├── Vaqt # app binary (arm64)
├── Info.plist # CFBundleName=Vaqt, version 1.0.3
├── Assets.car # .xcassets → actool
├── Base.lproj/Localizable.strings # 3 til + Base
├── en.lproj/Localizable.strings
├── ru.lproj/Localizable.strings
├── uz.lproj/Localizable.strings
├── _CodeSignature/CodeResources # adhoc (simulator)
├── Frameworks/
│ └── Core.framework/
│ ├── Core # umumiy dinamik framework
│ ├── CoreResources.bundle/prayers.json # namespaced resurs
│ ├── Info.plist
│ └── _CodeSignature/CodeResources
└── PlugIns/
└── VaqtWidget.appex/
├── VaqtWidget # widget binary
├── Assets.car
├── Info.plist # NSExtensionPointIdentifier
└── _CodeSignature/CodeResources
Vaqt.app ichidagi haqiqiy tuzilma va har bir qism qaysi BUILD rule'dan kelgani: binary, plist, resurs, struktura va imzo qismlariga ajratilgan
Diagramma har bir qismni uni yaratgan rule bilan bog'laydi. Xcode'da bu bog'lanishlar project.pbxproj ichida yashiringan va qo'lda sozlangan bo'lardi; Bazel'da esa har biri BUILD faylida bitta satr.
O'lchovlar (Apple M4 Pro, 12 core, Xcode 27.1, rules_apple 5.3.0)
Build tezligini o'lchadik. Eng ishonchli ko'rsatkich — qayta kompilyatsiya qilinadigan modullar soni (jarayon va worker soni), chunki wall-clock vaqti shu mashinada boshqa parallel build'lar bilan bo'lishilgani uchun tebranadi:
| O'lchov | Jarayonlar | SwiftCompile (worker) | Wall-clock\* |
|---|---|---|---|
| To'liq build (toza analiz, disk cache qisman issiq) | 398 action | 3 | ~8 s |
| No-op rebuild (hech narsa o'zgarmagan) | 0 action | 0 | 0.2–0.4 s |
Inkremental: App leaf (ContentView.swift) | 10 process | 1 (AppLib) | ~2–4 s |
| Inkremental: Core framework interfeysi | 17 process | 3 (Core+App+Widget) | ~2–11 s |
\*Wall-clock vaqti parallel agent'lar yukiga qarab tebranadi; barqaror signal — worker/action soni.
Eng muhim xulosa bu yerda: leaf o'zgarishi faqat 1 ta Swift modulni (AppLib) qayta kompilyatsiya qiladi; Core framework interfeysi o'zgarsa esa 3 ta modul (Core + AppLib + VaqtWidget) qayta kompilyatsiya qilinadi — chunki ikkalasi ham Core'ga bog'langan. Bu — dependency graph'ning kuchi: Bazel aynan nima o'zgarganini va unga nima bog'liqligini biladi, faqat o'shalarni qayta quradi. No-op rebuild esa 0.2 soniyada tugaydi, chunki Bazel hech narsa o'zgarmaganini biladi.
📊 Real raqamlar: Core framework interfeysini o'zgartirish
bazel query 'rdeps(//..., //Core:CoreLib)'ko'rsatgan 6 ta target'ga ta'sir qiladi://Core:CoreLib,//Core:Core,//App:AppLib,//App:Vaqt,//Widget:WidgetLib,//Widget:VaqtWidget. Lekin interfeysi o'zgarmagan (masalan faqat implementatsiya ichidagi) o'zgarish downstream modullarga action cache hit beradi — ular qayta kompilyatsiya qilinmaydi. Buni 10-darsda (action key) chuqurroq ko'ramiz.
🚀 Motivatsiya: Siz endi haqiqiy iOS ilova anatomiyasini to'liq egallab oldingiz: app + framework + widget extension, 3 tilli lokalizatsiya, asset catalog, namespaced resurslar, App Group entitlements, versiyalash va signing — hammasi bitta
bazel buildbilan, deklarativ, qayta-ishlab chiqariladigan (reproducible) tarzda. Bu — yuzlab modulli loyihalarda ham xuddi shunday ishlaydigan poydevor.
Amaliy mashqlar {#mashqlar}
Quyidagi mashqlarni o'z VaqtApp nusxangizda bajaring. Har birida .ipa ni unzip -l bilan ochib, natijani tekshiring.
- Yangi resurs qo'shing. Core framework'ga ikkinchi JSON fayl (
cities.json) qo'shing:Core/Resources/cities.jsonyarating, uniapple_resource_bundleningresourcesro'yxatiga qo'shing. Build qiling vaCoreResources.bundleichida ikkala JSON ham borligini tasdiqlang.PrayerStoregaloadCities()metodini yozib, uni o'qing. - To'rtinchi til.
tr.lproj/Localizable.strings(turkcha) qo'shing vaContentViewgalocalizedTitle("next_prayer", lang: "tr")qatorini qo'shing. Build qilib,.appichidatr.lprojpaydo bo'lganini ko'ring. Keyinbazel build //App:Vaqt --define "apple.locales_to_include=uz"bilan quring va faqatuz.lproj+Base.lprojqolganini tasdiqlang. - Versiyani o'zgartiring.
apple_bundle_versiondabuild_versionni1.0.4ga o'zgartiring, build qiling vaPlistBuddy -c "Print :CFBundleVersion"bilan app ham, widget ham1.0.4ekanini tekshiring. So'ngbuild_label_patternvariantiga o'tib,--embed_label=Vaqt_2.0_build_50bilan quring vaCFBundleVersion=2.0.50chiqishini tasdiqlang. - ⭐ Framework'ni statik qiling.
Coreniios_frameworko'rnigaswift_library(ya'ni faqatCoreLib) sifatida app va widget'ga to'g'ridan-to'g'ridepsorqali ulang (framework'ni olib tashlang). Build qilib, bundle'da nima o'zgarganini kuzating:Frameworks/Core.frameworkyo'qoladi,CoreResources.bundleendi qayerga tushadi? App va widget hajmi qanday o'zgardi (ls -la)? Nega? - **⭐
select()bilan dev build.**config_settingvaselect()ishlatib,-c dbgdabundle_idga.devqo'shiladigan vaCFBundleDisplayName"Vaqt DEV" bo'ladigan build yarating (Info.debug.plistfragmenti bilan).bazel build //App:Vaqt -c dbgva-c optbuild'larini solishtiring. - ⭐ Ikkinchi extension. Share extension (
ios_extension, plug-in modeli) qo'shing. Diqqat: u@mainemas,NSExtensionPrincipalClassishlatadi — vaPRODUCT_MODULE_NAMEalmashtirilmagani uchun modul nomini (ShareExt.ShareViewControllerkabi) qo'lda yozishingiz kerak.PlugIns/da endi ikkita.appexborligini tasdiqlang.
O'zingizni tekshiring {#checklist}
- [ ]
data(kutubxona resursi, app ildiziga tushadi) va top-level atributlar (app_icons,infoplists,strings) farqini tushunaman. - [ ]
apple_resource_bundlebilan namespaced bundle yarata olaman va u resurs egasi joylashgan bundle (framework yoki app) ichiga tushishini bilaman. - [ ]
.lprojlokalizatsiya qo'shib,locales_to_include/apple.trim_lproj_localesbilan tillarni cheklay olaman;Base.lprojhar doim qo'shilishini bilaman. - [ ]
ios_framework(+extension_safe = True) bilan umumiy kodni app va extension orasida bo'lisha olaman;depsvaframeworksfarqini bilaman. - [ ]
ios_extensionbilan WidgetKit widget quraman,NSExtensionPointIdentifierni to'g'ri yozaman va.app/PlugIns/*.appexni tekshiraman. - [ ] App Group entitlements'ni app va widget'ga ulab, simulatorda uning Mach-O
__entitlementssegmentida ekaniniotoolbilan tasdiqlay olaman. - [ ]
apple_bundle_versionbilan versiyani (hard-coded yoki--embed_label) boshqara olaman va extension versiyasi app bilan mos kelishini bilaman. - [ ] Simulator (adhoc, hisob kerak emas) va qurilma (
provisioning_profile+ signing identity) signing farqini tushunaman.
Xulosa {#xulosa}
- Resurs qayerga tushishi uni qanday e'lon qilishingizga bog'liq:
data→ app ildizi (Bundle.main);apple_resource_bundle→ namespaced.bundle(umumiy framework uchun eng yaxshi);apple_resource_group'ningstructured_resources→ katalog saqlanadi;app_icons/infoplists/strings→ OS resurslari sifatida alohida ishlanadi. - **rules_swift
Bundle.moduleni yaratmaydi** — bu SwiftPM'ga xos. Bazel'daBundle.mainyokiapple_resource_bundle+Bundle(for:)/Bundle(url:)ishlating. - Lokalizatsiya
.lprojpapkalari orqali;Base.lprojhar doim qo'shiladi;locales_to_includebilan debug tezligi uchun tillarni cheklash mumkin. - **
ios_framework(+extension_safe)** umumiy kodni bir marta qadoqlaydi — app ham, widget hamframeworksorqali bir nusxani ishlatadi (@rpath/Core.framework/Core).depsstatik link,frameworksdinamik embedding. - **WidgetKit widget =
ios_extension**@main+NSExtensionPointIdentifier = com.apple.widgetkit-extensionbilan;.app/PlugIns/*.appexga tushadi.extensionkit_extensionni WidgetKit uchunFalseqoldiring. - Entitlements (App Groups) app va widget'da bir xil bo'lishi kerak; simulatorda ular imzo blokiga emas, Mach-O
__entitlementssegmentiga yoziladi (otoolbilan tekshiriladi). - Versiya
apple_bundle_versionorqali bir joyda; signing simulatorda adhoc (hisob kerak emas), qurilmadaprovisioning_profile+ sertifikat. - Dependency graph tejaydi: leaf o'zgarishi 1 modulni, framework interfeysi o'zgarishi 3 modulni qayta kompilyatsiya qildi; no-op rebuild 0.2 soniya. Bitta
bazel build398 action bilan to'liq ilovani (app + framework + widget + resurs + imzo) yasadi. - Keyingi dars: Bazel 9: Xcode bilan ishlash — rules_xcodeproj, indexing, debugging, SwiftUI Preview va VS Code
Manbalar {#manbalar}
- [rules_apple — rules-ios.md (
ios_application,ios_extension,ios_framework)](https://github.com/bazelbuild/rules_apple/blob/main/doc/rules-ios.md) - [rules_apple — resources.md (resurslar falsafasi:
datavs top-level, resource bundle'lar)](https://github.com/bazelbuild/rules_apple/blob/main/doc/resources.md) - [rules_apple — rules-resources.md (
apple_resource_bundle,apple_resource_group,apple_bundle_import)](https://github.com/bazelbuild/rules_apple/blob/main/doc/rules-resources.md) - [rules_apple — common_info.md (Info.plist merging va o'zgaruvchilar, lokalizatsiya flaglari, codesigning,
--modify_execution_info)](https://github.com/bazelbuild/rules_apple/blob/main/doc/common_info.md) - [rules_apple — rules-general.md (
apple_bundle_version)](https://github.com/bazelbuild/rules_apple/blob/main/doc/rules-general.md) - [rules_apple — rules-apple.md (
local_provisioning_profile,provisioning_profile_repository)](https://github.com/bazelbuild/rules_apple/blob/main/doc/rules-apple.md) - [rules_xcodeproj — FAQ (code signing:
xcode_provisioning_profile,managed_by_xcode)](https://github.com/MobileNativeFoundation/rules_xcodeproj/blob/main/docs/faq.md) - rules_ios sunset — Discussion #952 (Square, 2025-11-07): rules_apple + rules_swift yetarli
- Tuist case study — Toss (~1000 modul, graph hisoblash narxi)
- Tuist case study — Monzo (~50 → ~200 modul)