Azimjon Abdurasulov

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_bundle bilan namespaced resurslar, ios_framework orqali 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 .ipa ichi 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 .app ichida 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, .lproj lokalizatsiya va apple_resource_bundle ni to'g'ri joylashtirasiz va kod ularni runtime'da qanday topishini tushunasiz.
  • ios_framework bilan Core kodini app va widget orasida bo'lishasiz; ios_extension bilan WidgetKit widget qo'shasiz.
  • Entitlements (App Groups), apple_bundle_version, select() bilan muhitga qarab sozlash va simulator vs qurilma signing'ni farqlaysiz.

Mundarija

  1. Nega "haqiqiy ilova" boshqacha
  2. [Resurslar: data va top-level atributlar](#resurslar)
  3. [Lokalizatsiya: .lproj va .strings](#lokalizatsiya)
  4. Resource bundle: namespaced resurslar
  5. Framework: kodni app va extension orasida bo'lishish
  6. [Widget extension: WidgetKit va ios_extension](#widget)
  7. Entitlements va App Groups
  8. Versiyalash va build konfiguratsiyalari
  9. Code signing: simulator va qurilma
  10. To'liq anatomiya va o'lchovlar
  11. Amaliy mashqlar
  12. O'zingizni tekshiring
  13. Xulosa
  14. 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 ichida Frameworks/ (umumiy kod) va PlugIns/ (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:Vaqt buyrug'ingiz app binary'ni, uning ichidagi Core.framework ni, VaqtWidget.appex widget'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 .ipa ni 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_apple va rules_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 faqat rules_apple + rules_swift ishlatamiz — 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_apple 5.3.0 bazel_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 runner xctestrun runner'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:

  1. Kutubxona resurslari — kod runtime'da o'qiydigan fayllar (JSON, rasm, CoreData model). Bular swift_library/objc_library ning **data** atributi orqali e'lon qilinadi.
  2. 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.module ni avtomatik yaratmaydi.** SwiftPM'da Bundle.module kutubxonaning o'z resurs bundle'iga ishora qiladi; Bazel'da esa data resurslari standart holatda app ildiziga tushadi va Bundle.main orqali topiladi. Agar kodingizda Bundle.module yozsangiz, kompilyatsiya xatosi olasiz. Buning yechimi — keyingi bo'limdagi apple_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_icons atributi data dan farq qiladi — unga faqat bitta *.xcassets/*.appiconset katalogini berasiz. Xcode 26+ uchun app_icons yangi Icon Composer *.icon bundle'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_include atributi (bu --define dan 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 .lproj papkalarini olib tashlaydi.

🧠 Esda tuting: Base.lproj har doim qo'shiladi, hatto locales_to_include da ko'rsatilmasa ham. Til nomlari aniq moslashtiriladi: pt yetarli emas, kerak bo'lishi mumkin pt_BR yoki pt_PT — chunki papka nomi aynan shunday. Bu --define da ham, locales_to_include atributida 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. CoreResources ni CoreLibning data siga uladik; CoreLib esa Core framework'ga kiradi. Shuning uchun CoreResources.bundle framework 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):

RuleNima qiladiQachon ishlatiladi
apple_resource_bundleBir nechta fayldan .bundle quradiUmumiy framework resurslari (bizning holat)
apple_bundle_importAllaqachon tayyor .bundle papkasini import qiladiChecked-in tayyor bundle
apple_resource_groupResurslarni guruhlaydi, structured_resources bilan katalog strukturasini saqlaydiSubkatalog 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, .png kabi "qayta ishlanadigan" fayllar bundling vaqtida optimallashtiriladi (masalan .png siqiladi, .strings binary plist'ga aylanadi). structured_resources esa 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_resources ishlating.


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_framework bor.


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:

  1. U @main orqali ishga tushadi (an'anaviy main kirish nuqtasi) — ko'pchilik boshqa extension'lar esa "plug-in" modelida ishlaydi, kirish nuqtasini system framework beradi.
  2. Uning Info.plist ida NSExtensionPointIdentifier = com.apple.widgetkit-extension bo'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 @main ishlatgani uchun NSExtensionPrincipalClass kerak emas. Lekin plug-in asosidagi extension'larda (Share, Notification Service) Info.plist da NSExtensionPrincipalClass = $(PRODUCT_MODULE_NAME).MyClass yoziladi. Diqqat: rules_apple **PRODUCT_MODULE_NAME ni almashtirmaydi** (common_info.md buni aniq aytadi), chunki Bazel'da har bir target alohida modul. Shuning uchun plug-in extension'da modul nomini qo'lda yozishingiz kerak, masalan VaqtWidgetExt.MyClass.

⚠️ Diqqat: ios_extension da extensionkit_extension degan atribut bor (default False). U ExtensionKit (iOS 16+ yangi model) extension'lari uchun. WidgetKit widget'lari ExtensionKit emas — ular an'anaviy NSExtension plug-in modelida, @main kirish nuqtasi bilan ishlaydi. Shuning uchun biz extensionkit_extension ni o'zgartirmaymiz (False qoldiramiz). Buni noto'g'ri True qilsangiz, 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 --entitlements emas, otool -s __TEXT __entitlements ishlating — birinchisi (adhoc) imzo blokini ko'rsatadi, u yerda faqat get-task-allow bor; 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.md aytadi: "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.framework ichidagi prayers.json ni 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_version target'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 u Vaqt ga aylandi (tekshirilgan). Lekin $(PRODUCT_MODULE_NAME) qo'llab-quvvatlanmaydi — uni qo'lda yozing.

⚠️ Diqqat: select() konfiguratsiya action key'ning bir qismi. Ya'ni -c opt va -c dbg build'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_profile shart emas — Apple hisobi ham, sertifikat ham kerak emas.
  • rules_apple bundle'ni adhoc imzolaydi (Signature=adhoc, TeamIdentifier=not set).
  • bazel run //App:Vaqt ilovani 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_profile atributini oladi, odatda local_provisioning_profile orqali 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_profile profilni mashinangizdagi ~/Library/MobileDevice/Provisioning Profiles dan 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_name bilan 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_id ni 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'lchovJarayonlarSwiftCompile (worker)Wall-clock\*
To'liq build (toza analiz, disk cache qisman issiq)398 action3~8 s
No-op rebuild (hech narsa o'zgarmagan)0 action00.2–0.4 s
Inkremental: App leaf (ContentView.swift)10 process1 (AppLib)~2–4 s
Inkremental: Core framework interfeysi17 process3 (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 build bilan, 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.

  1. Yangi resurs qo'shing. Core framework'ga ikkinchi JSON fayl (cities.json) qo'shing: Core/Resources/cities.json yarating, uni apple_resource_bundle ning resources ro'yxatiga qo'shing. Build qiling va CoreResources.bundle ichida ikkala JSON ham borligini tasdiqlang. PrayerStore ga loadCities() metodini yozib, uni o'qing.
  2. To'rtinchi til. tr.lproj/Localizable.strings (turkcha) qo'shing va ContentView ga localizedTitle("next_prayer", lang: "tr") qatorini qo'shing. Build qilib, .app ichida tr.lproj paydo bo'lganini ko'ring. Keyin bazel build //App:Vaqt --define "apple.locales_to_include=uz" bilan quring va faqat uz.lproj + Base.lproj qolganini tasdiqlang.
  3. Versiyani o'zgartiring. apple_bundle_version da build_version ni 1.0.4 ga o'zgartiring, build qiling va PlistBuddy -c "Print :CFBundleVersion" bilan app ham, widget ham 1.0.4 ekanini tekshiring. So'ng build_label_pattern variantiga o'tib, --embed_label=Vaqt_2.0_build_50 bilan quring va CFBundleVersion=2.0.50 chiqishini tasdiqlang.
  4. ⭐ Framework'ni statik qiling. Core ni ios_framework o'rniga swift_library (ya'ni faqat CoreLib) sifatida app va widget'ga to'g'ridan-to'g'ri deps orqali ulang (framework'ni olib tashlang). Build qilib, bundle'da nima o'zgarganini kuzating: Frameworks/Core.framework yo'qoladi, CoreResources.bundle endi qayerga tushadi? App va widget hajmi qanday o'zgardi (ls -la)? Nega?
  5. **⭐ select() bilan dev build.** config_setting va select() ishlatib, -c dbg da bundle_id ga .dev qo'shiladigan va CFBundleDisplayName "Vaqt DEV" bo'ladigan build yarating (Info.debug.plist fragmenti bilan). bazel build //App:Vaqt -c dbg va -c opt build'larini solishtiring.
  6. ⭐ Ikkinchi extension. Share extension (ios_extension, plug-in modeli) qo'shing. Diqqat: u @main emas, NSExtensionPrincipalClass ishlatadi — va PRODUCT_MODULE_NAME almashtirilmagani uchun modul nomini (ShareExt.ShareViewController kabi) qo'lda yozishingiz kerak. PlugIns/ da endi ikkita .appex borligini tasdiqlang.

O'zingizni tekshiring {#checklist}

  • [ ] data (kutubxona resursi, app ildiziga tushadi) va top-level atributlar (app_icons, infoplists, strings) farqini tushunaman.
  • [ ] apple_resource_bundle bilan namespaced bundle yarata olaman va u resurs egasi joylashgan bundle (framework yoki app) ichiga tushishini bilaman.
  • [ ] .lproj lokalizatsiya qo'shib, locales_to_include / apple.trim_lproj_locales bilan tillarni cheklay olaman; Base.lproj har doim qo'shilishini bilaman.
  • [ ] ios_framework (+extension_safe = True) bilan umumiy kodni app va extension orasida bo'lisha olaman; deps va frameworks farqini bilaman.
  • [ ] ios_extension bilan WidgetKit widget quraman, NSExtensionPointIdentifier ni to'g'ri yozaman va .app/PlugIns/*.appex ni tekshiraman.
  • [ ] App Group entitlements'ni app va widget'ga ulab, simulatorda uning Mach-O __entitlements segmentida ekanini otool bilan tasdiqlay olaman.
  • [ ] apple_bundle_version bilan 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'ning structured_resources → katalog saqlanadi; app_icons/infoplists/strings → OS resurslari sifatida alohida ishlanadi.
  • **rules_swift Bundle.module ni yaratmaydi** — bu SwiftPM'ga xos. Bazel'da Bundle.main yoki apple_resource_bundle + Bundle(for:)/Bundle(url:) ishlating.
  • Lokalizatsiya .lproj papkalari orqali; Base.lproj har doim qo'shiladi; locales_to_include bilan debug tezligi uchun tillarni cheklash mumkin.
  • **ios_framework (+extension_safe)** umumiy kodni bir marta qadoqlaydi — app ham, widget ham frameworks orqali bir nusxani ishlatadi (@rpath/Core.framework/Core). deps statik link, frameworks dinamik embedding.
  • **WidgetKit widget = ios_extension** @main + NSExtensionPointIdentifier = com.apple.widgetkit-extension bilan; .app/PlugIns/*.appex ga tushadi. extensionkit_extension ni WidgetKit uchun False qoldiring.
  • Entitlements (App Groups) app va widget'da bir xil bo'lishi kerak; simulatorda ular imzo blokiga emas, Mach-O __entitlements segmentiga yoziladi (otool bilan tekshiriladi).
  • Versiya apple_bundle_version orqali bir joyda; signing simulatorda adhoc (hisob kerak emas), qurilmada provisioning_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 build 398 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}

  1. [rules_apple — rules-ios.md (ios_application, ios_extension, ios_framework)](https://github.com/bazelbuild/rules_apple/blob/main/doc/rules-ios.md)
  2. [rules_apple — resources.md (resurslar falsafasi: data vs top-level, resource bundle'lar)](https://github.com/bazelbuild/rules_apple/blob/main/doc/resources.md)
  3. [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)
  4. [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)
  5. [rules_apple — rules-general.md (apple_bundle_version)](https://github.com/bazelbuild/rules_apple/blob/main/doc/rules-general.md)
  6. [rules_apple — rules-apple.md (local_provisioning_profile, provisioning_profile_repository)](https://github.com/bazelbuild/rules_apple/blob/main/doc/rules-apple.md)
  7. [rules_xcodeproj — FAQ (code signing: xcode_provisioning_profile, managed_by_xcode)](https://github.com/MobileNativeFoundation/rules_xcodeproj/blob/main/docs/faq.md)
  8. rules_ios sunset — Discussion #952 (Square, 2025-11-07): rules_apple + rules_swift yetarli
  9. Tuist case study — Toss (~1000 modul, graph hisoblash narxi)
  10. Tuist case study — Monzo (~50 → ~200 modul)

Markdown manba

iOS Lessons