Azimjon Abdurasulov

Bazel 4: Starlark tili — BUILD va .bzl fayllar, macro'lar, select() va konfiguratsiya

📦 Bazel iOS — Mukammal Qo‘llanma & Amaliyot

Bazel 4: Starlark tili — BUILD va .bzl fayllar, macro'lar, select() va konfiguratsiya

Holat: 2026-yil oktabr. Bu darsda Bazel'ning tili — Starlark'ni noldan o'rganamiz: BUILD va .bzl fayllar, konstantalar, funksiyalar, macro'lar, select() va build setting'lar bilan bitta kod bazasini turli muhitlar (dev/staging/prod) uchun quramiz. Kod misollari Bazel 9.3.0, rules_apple 5.3.0, rules_swift 4.2.1, bazel_skylib 1.9.2 va Xcode 27.1 bilan Apple M4 Pro'da tekshirilgan.

Bu darsdan keyin siz: BUILD fayllaringizdagi takrorlanishni macro bilan yo'qotasiz, bitta kodni select() orqali bir nechta muhit uchun quradigan qilib sozlaysiz va .bzl fayllarini xotirjam yoza olasiz.

  • Starlark'ning Python'dan qaysi 10 ta joyda farq qilishini va nega shunday qilinganini tushunasiz.
  • BUILD faylida nima qilsa bo'ladi, nima qilib bo'lmaydi — va nega funksiya .bzl'ga ketadi.
  • ios_module(name, srcs, test_srcs) — bitta chaqiriqdan swift_library + test kutubxona + ios_unit_test yasaydigan symbolic macro yozasiz.
  • select() + config_setting + string_flag bilan API_BASE_URL'ni build vaqtida tanlaysiz (dev → staging → prod), bironta #if'ni qo'lda o'zgartirmasdan.
  • print(), fail() va buildifier bilan Starlark kodini debug va lint qilasiz.

🚀 Motivatsiya: Xcode loyihasida "Debug" va "Release" uchun ikki xil API_BASE_URL kerak bo'lsa, odatda #if DEBUG yoki .xcconfig fayllari bilan ovora bo'lasiz — va qaysi target qaysi qiymatni olishini project.pbxproj ichida qidirasiz. Bazel'da bu bitta select() — va biz uni shu darsda haqiqiy CLI'da ishga tushirib, dev → staging → prod o'tishini o'z ko'zimiz bilan ko'ramiz.


Mundarija

  1. Starlark nima va nega Python emas
  2. [BUILD fayl vs .bzl fayl — mehnat taqsimoti](#build-vs-bzl)
  3. [Starlark tili: tiplar, funksiya, comprehension, load()](#til)
  4. Macro nima va nega kerak
  5. Legacy macro — eski uslub
  6. [Symbolic macro va ios_module](#symbolic-macro)
  7. [Macro'ni "ochib ko'rish": query --output=build](#macro-expansion)
  8. [select() — build vaqtidagi tanlov](#select)
  9. [config_setting va o'z build setting'laringiz](#build-settings)
  10. [--define, -c opt, platforms — konfiguratsiya xaritasi](#other-config)
  11. Konfiguratsiya action key'ga qanday ta'sir qiladi
  12. [Starlark'ni debug qilish: print(), fail(), buildifier](#debugging)
  13. Amaliy mashqlar
  14. O'zingizni tekshiring
  15. Xulosa
  16. Manbalar

1. Starlark nima va nega Python emas {#starlark-nima}

Oldingi darsda (Bazel 3: Asosiy tushunchalar) biz BUILD.bazel fayllariga swift_library, ios_application kabi rule'larni yozdik. Lekin aslida o'sha fayllar oddiy matn emas — ular kichik dastur. Bazel ularni o'qiganda bir tilni ishga tushiradi: Starlark.

Starlark — Google ichida Blaze (Bazel'ning ichki ajdodi) uchun yozilgan, Python3'dan ilhomlangan til. Ilgari u "Skylark" deb atalgan. Bazel'ning rasmiy hujjatlarida u shunday ta'riflanadi: Starlark sintaksisi Python3'dan ilhomlangan va bir xil ko'rinadigan kod ko'pincha bir xil ishlaydi — lekin ataylab ba'zi joylarda cheklangan (manba: Bazel docs, rules/language).

Nega Python'ning o'zini ishlatmagan? Chunki build tizimi uchta narsani kafolatlashi kerak:

  1. Deterministik (aniq natija). Bir xil kirish → har doim bir xil chiqish. Agar BUILD fayli tasodifiy son, hozirgi vaqt yoki diskdagi faylga qarab har safar boshqacha target yasasa, cache ishonchsiz bo'lib qoladi.
  2. Hermetik (yopiq). BUILD/.bzl kodi ixtiyoriy I/O qila olmaydi — tarmoqqa chiqa olmaydi, faylni o'qiy olmaydi, env o'zgaruvchisini "yashirincha" ololmaydi. Shu sababli build qayta-qayta takrorlanadigan bo'ladi (manba: Bazel docs, concepts/build-files → Hermeticity).
  3. Parallel va xavfsiz. Bazel minglab .bzl va BUILD fayllarini bir vaqtda o'qiydi. Agar bir fayl ikkinchisining global o'zgaruvchisini o'zgartira olsa, bu poyga (race) bo'lardi.

Shuning uchun Starlark Python'dan quyidagi joylarda qasddan farq qiladi (hammasi rules/language hujjatidan):

Python'da borStarlark'daNega
while, generator, yieldyo'qcheksiz / noaniq ish vaqtining oldini olish
Rekursiya (funksiya o'zini chaqiradi)yo'qbuild grafi aniq va chekli bo'lishi uchun
classyo'q (o'rniga struct)sodda, yagona ma'lumot modeli
importyo'q (o'rniga load())bog'liqliklar aniq ko'rinishi uchun
try / except / raiseyo'q (o'rniga fail())xatoni "yutib" yuborish mumkin emas
Global o'zgaruvchini o'zgartirishyo'q — load'dan keyin muzlaydiparallel ishlashda xavfsizlik
float, setyo'qdeterminizm (float dumaloqlash muammolari)
5 < 'foo' (tiplararo solishtirish)xatoyashirin bug'larning oldini olish
1 < x < 5 (zanjirli solishtirish)yo'qsoddalik
int cheksiz kattalikda32-bit (overflow → xato)determinizm

🧠 Esda tuting: Starlark Python emas, u Python'ga o'xshaydi. Siz Python bilgansiz deb o'ylab while yoki class yozsangiz, Bazel darhol xato beradi. "Python kabi, lekin ataylab qisqartirilgan konfiguratsiya tili" — eng to'g'ri aqliy model shu.

Eng muhim xususiyat — immutability (o'zgarmaslik). rules/language hujjatidagi klassik misol: .bzl faylida e'lon qilingan global ro'yxat, fayl o'qib bo'linganidan keyin muzlaydi. Boshqa fayl uni load qilib, ichiga qiymat qo'sha olmaydi:

# foo.bzl
var = []            # ro'yxat e'lon qilamiz

def fct():
    var.append(5)   # ichiga qiymat qo'shamiz

fct()               # funksiyani chaqiramiz -> var endi [5]
# bar.bzl
load(":foo.bzl", "var", "fct")

var.append(6)  # XATO: foo.bzl tugagach, var muzlagan (frozen)
fct()          # XATO: fct() muzlagan ro'yxatni o'zgartirmoqchi

Buning sababi: har bir .bzl va BUILD fayl o'z kontekstida ishlaydi va Bazel ularni parallel bajaradi. Agar bar.bzl foo.bzl'ning holatini o'zgartira olsa, natija qaysi fayl avval o'qilganiga bog'liq bo'lib qolardi — ya'ni nodeterministik.

2. BUILD fayl vs .bzl fayl — mehnat taqsimoti {#build-vs-bzl}

Bazel'da ikki xil Starlark fayli bor va ularning vazifasi aniq bo'lingan:

  • **BUILD (yoki BUILD.bazel) — deklarativ ro'yxat. Bu yerda siz faqat target e'lon qilasiz**: swift_library(...), ios_application(...). BUILD faylida funksiya yozib bo'lmaydi, top-level for/if yozib bo'lmaydi, *args/**kwargs ishlatib bo'lmaydi.
  • **.bzl (Bazel extension)** — ta'riflar. Bu yerda konstantalar, funksiyalar, macro'lar va rule'lar yashaydi. .bzl faylidan tayyor narsalarni BUILD'ga load() orqali olib kirasiz.

Rasmiy hujjat buni shunday izohlaydi (concepts/build-files): "Kod va ma'lumotni toza ajratish uchun BUILD fayllarida funksiya e'loni, for va if operatorlari bo'lishi mumkin emas (lekin list comprehension va if ifodasi mumkin). Funksiyalarni .bzl fayllarida e'lon qilish kerak."

Keling, buni haqiqiy xato bilan ko'rsatamiz. Agar BUILD.bazel fayliga funksiya yozsak:

# scratch_def/BUILD.bazel
def helper():
    return 1
bazel query //scratch_def:all
ERROR: .../scratch_def/BUILD.bazel:1:1: functions may not be defined in BUILD
files. You may move the function to a .bzl file and load it.
ERROR: package contains errors: scratch_def: functions may not be defined in
BUILD files. You may move the function to a .bzl file and load it.

Xato to'g'ridan-to'g'ri yechimni aytyapti: funksiyani .bzl fayliga ko'chir va load qil.

BUILD fayl deklarativ target ro'yxati, .bzl fayl esa funksiya, macro, rule va konstantalarni saqlaydi; load() ular orasidagi ko'prik

Yuqoridagi rasm ikki faylning rolini ko'rsatadi: chapda BUILD — faqat target e'loni, o'ngda .bzl — mantiq (konstanta, funksiya, macro, rule), va ular orasida bitta ko'prik: load().

🧠 Esda tuting: Oddiy qoida — **BUILD'da nima quriladi, .bzl'da qanday quriladi.** BUILD'ni odam ham, vosita ham (masalan, bazel query yoki IDE) oson o'qishi kerak; shuning uchun uni sodda va deklarativ saqlang.

load() sintaksisi (concepts/build-filesdan):

# bitta simvolni yuklash
load("//tools:defs.bzl", "ios_module")

# bir nechta simvol + taxallus (alias)
load(":my_rules.bzl", "some_rule", nice_alias = "some_other_rule")

Birinchi argument — .bzl faylining label'i (//tools:defs.bzl). Qolganlari — olib kiriladigan simvol nomlari (qo'shtirnoq ichida, o'zgaruvchi emas). load faqat faylning eng yuqorisida (top-level) turishi mumkin, funksiya ichida emas. Va muhim nuqta: .bzl faylida _ bilan boshlanadigan simvollar eksport qilinmaydi — ularni boshqa fayl load qila olmaydi. Shuning uchun macro/rule'ning ichki implementatsiya funksiyasi odatda _ bilan boshlanadi (masalan _ios_module_impl).

3. Starlark tili: tiplar, funksiya, comprehension, load() {#til}

Endi tilning o'zini bir "demo" bilan ko'ramiz. Quyidagi .bzl faylida Starlark'ning asosiy qurilmalarini jamladik: konstanta, funksiya, for-siz ishlaydigan list comprehension, dict comprehension, %-formatlash va if-ifoda:

# lang/demo.bzl
"""Starlark tilini ko'rsatuvchi kichik demo funksiyalar."""

# Global konstanta - odatda UPPERCASE bo'ladi
COMMON_COPTS = ["-warnings-as-errors", "-enable-actor-data-race-checks"]

def module_names(prefix, count):
    """prefix asosida modul nomlari ro'yxatini yasaydi (list comprehension)."""
    return ["%s%d" % (prefix, i) for i in range(1, count + 1)]

def summarize(modules):
    """dict qaytaradi: har modulning nomi va uzunligi."""
    return {name: len(name) for name in modules}

def describe():
    mods = module_names("Feature", 3)
    sizes = summarize(mods)
    # print() - loading fazasida DEBUG log sifatida chiqadi
    print("modullar =", mods)
    print("uzunliklar =", sizes)
    print("birinchi modul =", mods[0] if mods else "yo'q")

Endi uni BUILD faylidan chaqiramiz (e'lon qila olmaymiz, lekin chaqira olamiz):

# lang/BUILD.bazel
load(":demo.bzl", "describe")

# BUILD faylida funksiya E'LON qilib bo'lmaydi, lekin .bzl dan
# yuklangan funksiyani CHAQIRISH mumkin.
describe()

bazel query //lang:all — bu paketni yuklaydi, ya'ni loading fazasini ishga tushiradi va describe() chaqiriladi:

bazel query //lang:all
DEBUG: .../lang/demo.bzl:18:10: modullar = ["Feature1", "Feature2", "Feature3"]
DEBUG: .../lang/demo.bzl:19:10: uzunliklar = {"Feature1": 8, "Feature2": 8, "Feature3": 8}
DEBUG: .../lang/demo.bzl:20:10: birinchi modul = Feature1
INFO: Empty results

E'tibor bering: describe() bironta target yasamagani uchun query natijasi bo'sh (Empty results), lekin print() chiqishlari loading fazasida ko'rindi. Bu Starlark kodingiz qachon ishlashini aniq ko'rsatadi: hali hech narsa kompilyatsiya qilinmasdan, Bazel faqat fayllarni o'qiyotganda.

Keling, chiqishni tahlil qilamiz:

  • ["%s%d" % (prefix, i) for i in range(1, count + 1)] — list comprehension. range(1, 4) → 1, 2, 3, va har biriga "Feature%d" formatlanadi → ["Feature1", "Feature2", "Feature3"]. Starlark top-level'da for operatorini taqiqlaydi, lekin comprehension'ga ruxsat beradi.
  • {name: len(name) for name in modules} — dict comprehension. Natija: {"Feature1": 8, ...}.
  • mods[0] if mods else "yo'q" — if-ifoda (statement emas, ifoda). Starlark'da if statement top-level'da yoki BUILD'da taqiqlangan, lekin bu ifoda shakli ruxsat etilgan.
  • "%s%d" % (prefix, i) — Python uslubidagi string formatlash. Starlark string'lari " qo'shtirnoq bilan yoziladi.

💡 Pro tip: Starlark string'lari iteratsiya qilinmaydi (for c in "abc" xato beradi) va yashirin string birlashtirish yo'q — "a" "b" emas, aniq "a" + "b" yozasiz. Agar C-dan yoki Python'dan kelgan odatlaringiz bilan yozsangiz, bu ikkisi eng ko'p uchraydigan "nega ishlamayapti?" lahzalari.

Qo'llab-quvvatlanadigan asosiy tiplar (rules/languagedan): None, bool, int (32-bit), string, list, dict, tuple, function. float va set yo'q.

4. Macro nima va nega kerak {#macro-nima}

Tasavvur qiling, loyihangizda 30 ta modul bor va har biri bir xil naqshdan iborat: kutubxona, uning test kutubxonasi, va ios_unit_test. Har bir modul uchun BUILD faylida uchta blok yozasiz — hammasi bir xil, faqat nomlar farq qiladi. Keyin kompaniya qarori chiqadi: "barcha modullar -warnings-as-errors bilan qurilsin". Endi 30 ta faylni qo'lda o'zgartirasiz. Bu — klassik iOS jamoasidagi og'riq.

Macro — aynan shu takrorlanishni yo'qotadi. Rasmiy ta'rif (extending/macros hujjati): "Macro — BUILD faylidan chaqiriladigan va rule'larni yaratadigan funksiya. Macro'lar asosan mavjud rule'larni enkapsulyatsiya qilish va qayta ishlatish uchun." Macro loading fazasida chaqiriladi; chaqiruv tugagach, Bazel deyarli faqat yakuniy, aniq rule target'larni ko'radi — xuddi siz ularni qo'lda yozganday.

Macro ikki xil bo'ladi (extending/macros):

  • Legacy macro — eski uslub. Oddiy Starlark funksiyasi; hech qanday maxsus qoidasi yo'q.
  • Symbolic macro — Bazel 8'da kiritilgan yangi tizim. Tipli (typed) argumentlar, label/select avtomatik konvertatsiyasi, va ichki target'larni yashirish (granular visibility) imkoniyati beradi.

Rasmiy tavsiya aniq (extending/macros): "Iloji bo'lsa, kodning aniqligi uchun symbolic macro'dan foydalaning." Biz ikkalasini ham yozamiz — avval farqni his qilish uchun legacy'ni, keyin asosiy qurol sifatida symbolic'ni.

⚠️ Diqqat: Macro — bepul emas. .bzl style guide ogohlantiradi (rules/bzl-style): macro ichidagi target'lar bazel query natijasida go'yoki o'z-o'zidan paydo bo'ladi (ularni BUILD faylida ko'rmaysiz), nosozlik chiqsa foydalanuvchi sizning macro implementatsiyangizni o'qishga majbur bo'ladi, va aspect'lar (IDE'lar ishlatadigan) macro'ni ko'rmaydi. Shuning uchun macro'ni faqat haqiqiy takror bo'lganda yozing — "ehtimol kerak bo'lar" uchun emas (YAGNI).

Buni yanada kuchliroq aytadigan nuqta rules/bzl-styleda bor: "BUILD fayllarida takrorlanishni kamaytirish uchun o'zgaruvchi yoki macro yaratmang... DRY printsipi bu yerda unchalik qo'llanmaydi." Ya'ni BUILD faylidagi bir oz takror — bu ko'pincha yaxshi, chunki har bir e'lon mustaqil o'qiladi. Macro'ni jiddiy, tuzilmaviy takror bo'lgandagina kiriting.

5. Legacy macro — eski uslub {#legacy-macro}

Legacy macro — shunchaki funksiya. Keling, bitta Swift modul + uning "support" kutubxonasini yasaydigan oddiy legacy macro yozamiz:

# tools/legacy.bzl
"""Eski uslubdagi (legacy) macro - taqqoslash uchun."""

load("@rules_swift//swift:swift_library.bzl", "swift_library")

# Legacy macro - oddiy funksiya. native. kerak emas, chunki swift_library load qilingan.
# Nom sxemasi majburiy emas: "CoreSupport" kabi nom ham ishlaydi.
def swift_module_legacy(name, srcs, deps = [], visibility = None):
    swift_library(
        name = name,
        srcs = srcs,
        module_name = name,
        deps = deps,
        visibility = visibility,
    )
    swift_library(
        name = name + "Support",  # symbolic macro bunga ruxsat bermasdi
        srcs = srcs,
        module_name = name + "Support",
        deps = [":" + name],
        visibility = visibility,
    )

BUILD faylida ishlatamiz:

# legacy/BUILD.bazel
load("//tools:legacy.bzl", "swift_module_legacy")

swift_module_legacy(
    name = "Greeter",
    srcs = ["Sources/Greeter.swift"],
)
bazel query //legacy:all
//legacy:Greeter
//legacy:GreeterSupport

Ikkala target ham yaratildi va GreeterSupport — Greeter'dan keyin hech qanday ajratuvchisiz (_, -, . yo'q) — bemalol ishladi. Buni yodda tuting: 3-bo'limda ko'ramizki, symbolic macro bunday nomga ruxsat bermaydi.

Legacy macro'ning kuchli va zaif tomonlari:

  • ➕ Juda moslashuvchan: istalgan Starlark mantiqi, erkin nom.
  • ➖ "Action at a distance": macro ichki target'larini boshqa paketdan ham ko'rish va ularga bog'lanish mumkin — ichki detallar oqib ketadi.
  • ➖ Argumentlar tipsiz: noto'g'ri qiymat bersangiz, xato chuqur ichkarida, tushunarsiz joyda chiqadi.
  • ➖ Visibility tizimi uchun shaffof: legacy macro ichidagi target go'yoki to'g'ridan-to'g'ri BUILD faylida yozilganday ko'rinadi.

💡 Pro tip: Eski Bazel versiyalarini (7.x) qo'llab-quvvatlashingiz kerak bo'lsa, legacy macro yagona yo'l — symbolic macro faqat Bazel 8'dan boshlab bor. Aks holda — symbolic macro.

6. Symbolic macro va ios_module {#symbolic-macro}

Endi kursimizning asosiy quroli: symbolic macro. U macro() funksiyasi bilan e'lon qilinadi va ikkita majburiy parametr oladi (extending/macros): attrs (tipli argumentlar) va implementation (mantiq).

Biz iOS jamoalarining klassik naqshini quramiz — ios_module: bitta chaqiriqdan uchta narsani yasaydi: asosiy swift_library, test swift_library (testonly), va ios_unit_test. Bu aynan Bazel 13: "Vazifalar" ilovasida 4 ta modulni qurishda ishlatadigan macro'imiz bo'ladi.

# tools/defs.bzl
"""iOS modul uchun symbolic macro: swift_library + test lib + ios_unit_test."""

load("@rules_apple//apple:ios.bzl", "ios_unit_test")
load("@rules_swift//swift:swift_library.bzl", "swift_library")

_DEFAULT_MIN_OS = "17.0"

def _ios_module_impl(name, visibility, srcs, deps, test_srcs, test_deps, minimum_os_version):
    if not srcs:
        fail("ios_module(%s): 'srcs' bo'sh bo'lishi mumkin emas" % name)

    # 1) Asosiy kutubxona - modulning o'zi
    swift_library(
        name = name,
        srcs = srcs,
        module_name = name,
        deps = deps,
        visibility = visibility,
    )

    # Test manbasi berilmasa, test target yaratmaymiz
    if not test_srcs:
        return

    # 2) Test kutubxonasi (testonly).
    # Symbolic macro ichidagi target nomi name'dan keyin '_' bilan boshlanishi SHART.
    swift_library(
        name = name + "_tests_lib",
        testonly = True,
        srcs = test_srcs,
        deps = [":" + name] + test_deps,
    )

    # 3) ios_unit_test - testlarni simulyatorda ishga tushiradigan target
    ios_unit_test(
        name = name + "_tests",
        minimum_os_version = minimum_os_version,
        deps = [":" + name + "_tests_lib"],
    )

ios_module = macro(
    doc = "Bitta iOS modul: swift_library + test kutubxona + ios_unit_test.",
    implementation = _ios_module_impl,
    attrs = {
        "srcs": attr.label_list(
            allow_files = [".swift"],
            mandatory = True,
            configurable = False,
        ),
        "deps": attr.label_list(),
        "test_srcs": attr.label_list(
            allow_files = [".swift"],
            configurable = False,
        ),
        "test_deps": attr.label_list(),
        "minimum_os_version": attr.string(
            default = _DEFAULT_MIN_OS,
            configurable = False,
        ),
    },
)

Diqqat qilinadigan joylar (hammasi extending/macrosga mos):

  • **implementation funksiyasi har bir argument uchun alohida parametr oladi** (rule implementatsiyasidan farqli — unda bitta ctx bo'ladi). Va u **name hamda visibility** parametrlarini olishi shart.
  • **attrs** — tipli. attr.label_list, attr.string va h.k. name va visibility avtomatik qo'shiladi, ularni attrs'ga yozmaysiz.
  • **configurable = False** — bu atributga select() berib bo'lmaydi degani. srcs'ni configurable=False qildik, chunki glob() natijasini kutyapmiz, select emas.
  • **mandatory = True** — srcs berilmasa, Bazel darhol xato beradi.
  • Symbolic macro qiymat qaytara olmaydi, argumentlarini o'zgartira olmaydi, glob() chaqira olmaydi. Shuning uchun glob() ni BUILD faylida chaqiramiz va natijani macro'ga uzatamiz.

BUILD faylida ishlatamiz:

# Feature/BUILD.bazel
load("//tools:defs.bzl", "ios_module")

# glob() BUILD faylida chaqiriladi - symbolic macro ichida emas.
ios_module(
    name = "Feature",
    srcs = glob(["Sources/**/*.swift"]),
    test_srcs = glob(["Tests/**/*.swift"]),
    visibility = ["//visibility:public"],
)

Haqiqiy xato: symbolic macro nom sxemasi

Birinchi urinishda biz test target'larini odatdagidek name + "Tests" → FeatureTests deb nomladik (ko'p iOS jamoalari shunday ataydi). Natija — bu real xato edi:

bazel query //Feature:all
Error: macro 'Feature' cannot declare submacro named 'FeatureTests'. Name must
be the same as the macro's name, or the macro's name followed by '_'
(recommended), '-', or '.', and a non-empty string.

Ikkita narsa ma'lum bo'ldi:

  1. Symbolic macro nom sxemasini majburlaydi (extending/macros): my_macro(name = "foo") faqat foo, yoki foo_..., foo-..., foo.... bilan boshlanadigan target yarata oladi. FeatureTests — Feature'dan keyin darhol T keladi, ajratuvchi yo'q → taqiqlangan. To'g'ri nom: Feature_tests.
  2. **ios_unit_testning o'zi — symbolic macro!** Xato "submacro" deb aytdi; demak rules_apple 5.3.0'da ios_unit_test ichida yana macro'lar chaqiriladi (manba: rules_apple apple/ios.bzl, macro_factory.create_apple_test_macro). Shuning uchun nom sxemasi qatlamma-qatlam tekshiriladi.

Yechim — _ ajratuvchidan foydalanish (name + "_tests", name + "_tests_lib"). Biz yuqoridagi kodda shuni qildik.

🧠 Esda tuting: Symbolic macro ichida yaratgan har bir target nomi name bilan boshlanib, keyin _/-/. ajratuvchisi kelishi shart. Bu — g'alati emas, balki foydali: Bazel qaysi macro qaysi target'ni yaratganini nomidan biladi va kelajakda "lazy evaluation"ni (faqat kerakli macro'ni ochish) shu bilan amalga oshiradi.

Symbolic macro visibility'ni qanday himoya qiladi

Symbolic macro'ning eng muhim ustunligi — ichki target'larni yashirish. Rasmiy qoida (extending/macros): macro ichida yaratilgan target, agar siz unga visibility bermasangiz, faqat macro'ning o'z paketiga ko'rinadi, macro chaqirilgan paketga emas.

Biz ios_module'da swift_libraryga visibility = visibility berdik (ya'ni chaqiruvchi so'ragan ko'rinish), lekin ios_unit_testga hech narsa bermadik. Natijada (keyingi bo'limdagi query --output=build chiqishida ko'rasiz) test target'ning visibility'si ["//tools:__pkg__"] bo'lib chiqadi — ya'ni macro e'lon qilingan //tools paketiga. Bu shuni anglatadi: boshqa modul tasodifan //Feature:Feature_testsga deps orqali bog'lana olmaydi. Ichki detal — ichki bo'lib qoladi.

🚀 Developer superpower: Legacy macro'da ichki target'lar butun workspace'ga ochiq edi ("action at a distance"). Symbolic macro bilan siz arxitekturani kod darajasida himoya qilasiz: modulning tashqi API'si (asosiy kutubxona) ko'rinadi, ichki test apparati esa yashirin. Hech kim sizning ichki target'ingizga bog'lanib, keyinroq refaktoringni buzib qo'ya olmaydi.

7. Macro'ni "ochib ko'rish": query --output=build {#macro-expansion}

Macro loading fazasida "ochiladi" (expand) — bitta chaqiruv bir nechta real target'ga aylanadi. Bu ochilishni o'z ko'zingiz bilan ko'rishning eng yaxshi yo'li — bazel query --output=build. Avval oddiy query bilan qaysi target'lar paydo bo'lganini sanaymiz:

bazel query //Feature:all
//Feature:Feature
//Feature:Feature_tests
//Feature:Feature_tests.__internal__.__test_bundle
//Feature:Feature_tests_lib

Bitta ios_module(name="Feature", ...) chaqiruvi to'rtta target yasadi: asosiy kutubxona (Feature), test target (Feature_tests), test bundle (ios_unit_test ichida yaratilgan __internal__ target), va test kutubxonasi (Feature_tests_lib).

Endi --output=build bilan ularning to'liq ko'rinishini olamiz (qisqartirilgan):

bazel query --output=build //Feature:all
# Feature/BUILD.bazel:4:11
swift_library(
  name = "Feature",
  visibility = ["//visibility:public"],
  generator_name = "Feature",
  generator_function = "ios_module",
  generator_location = "Feature/BUILD.bazel:4:11",
  deps = [],
  module_name = "Feature",
  srcs = ["//Feature:Sources/Task.swift"],
)
...
ios_unit_test(
  name = "Feature_tests",
  visibility = ["//tools:__pkg__"],
  generator_name = "Feature",
  generator_function = "ios_module",
  runner = "@rules_apple//apple/testing/default_runner:ios_default_runner",
  deps = ["//Feature:Feature_tests.__internal__.__test_bundle"],
  minimum_os_version = "17.0",
)
...
swift_library(
  name = "Feature_tests_lib",
  generator_name = "Feature",
  generator_function = "ios_module",
  testonly = True,
  deps = ["//Feature:Feature"],
  module_name = "Feature_tests_lib",
  ...
)

Bu chiqishda uchta qimmatli narsa bor:

  1. **generator_function = "ios_module" va generator_name = "Feature"** — har bir target qaysi macro tomonidan, qaysi chaqiruvdan yaratilganini aytadi. Nosozlik qidirganda bu oltin: bazel query --output=build 'attr(generator_function, ios_module, //...)' bilan macro yaratgan barcha target'larni topasiz.
  2. **Feature kutubxonasi visibility = ["//visibility:public"]** — biz visibility = visibility berganimiz uchun. Lekin **Feature_tests esa visibility = ["//tools:__pkg__"]** — biz unga hech narsa bermaganimiz uchun u macro paketiga (//tools) yashirildi. Bu symbolic macro visibility himoyasining amaldagi isboti.
  3. **runner = "@rules_apple//apple/testing/default_runner:ios_default_runner"** — ios_unit_test o'zi qo'shgan standart runner. rules_apple 5.x'da bu xctestrun runner (manba: rules_apple 5.0.0 release notes, "Switch default iOS test runner to xctestrun runner").

Endi macro haqiqatan ishlashiga ishonch hosil qilamiz — modulni quramiz va test'ni simulyatorda ishga tushiramiz:

bazel build //Feature:Feature
Target //Feature:Feature up-to-date:
  bazel-bin/Feature/Feature.swiftmodule
  bazel-bin/Feature/libFeature.a
INFO: Elapsed time: 3.956s
bazel test //Feature:Feature_tests --test_output=errors
//Feature:Feature_tests          PASSED in 55.4s

Executed 1 out of 1 test: 1 test passes.
INFO: Elapsed time: 67.005s

Birinchi test ~55 soniya oldi, chunki Bazel simulyator yaratib, uni yuklashi kerak bo'ldi (M4 Pro, Xcode 27.1). Lekin hech narsa o'zgarmasa, ikkinchi marta:

bazel test //Feature:Feature_tests
//Feature:Feature_tests          (cached) PASSED in 55.4s
INFO: Elapsed time: 0.212s
Executed 0 out of 1 test: 1 test passes.

**(cached) PASSED, 0.212 soniya, "Executed 0 out of 1 test".** Bazel test natijasini cache'dan oldi — hech narsa qayta ishlamadi. Test cache'i haqida to'liq Bazel 6: Testlarda gaplashamiz, lekin bu yerda muhimi: siz yozgan bitta macro → to'liq ishlaydigan, cache'lanadigan test infratuzilmasi.

ios_module(name=Feature) chaqiruvi loading fazasida to'rtta targetga ochiladi: Feature kutubxona, Feature_tests_lib test kutubxona, Feature_tests ios_unit_test va ichki test bundle

Rasm macro ochilishini ko'rsatadi: chapda BUILD faylidagi bitta ios_module(...) chaqiruvi, o'ngda loading fazasidan keyin paydo bo'lgan to'rtta real target va ular orasidagi deps bog'lanishlari.

8. select() — build vaqtidagi tanlov {#select}

Mana darsning eng amaliy qismi. Bir xil kodni turli muhitlar uchun quraylik: dev muhitida API http://localhost:8080, stagingda https://staging.api.vazifalar.uz, prodda https://api.vazifalar.uz bo'lsin. Xcode'da buni odatda .xcconfig fayllari yoki sxema (scheme) sozlamalari bilan qilasiz. Bazel'da — bitta select().

select() — Bazel'ning "configurable attribute" imkoniyati (manba: docs/configurable-attributes). U atribut qiymatini build vaqtidagi bayroqlarga qarab tanlaydi. Ichki mexanizm: select() — bu placeholder (o'rinbosar); analiz fazasida Bazel qaysi shart (config_setting) to'g'ri kelishini aniqlab, bitta shoxni tanlaydi.

Avval Swift tomonini yozamiz. API manzili kompilyatsiya vaqtidagi -D bayrog'iga qarab tanlanadi:

// cli/main.swift
// API manzili build vaqtida tanlanadi: -D STAGING / -D PROD / (hech narsa = dev)
#if PROD
let apiBaseURL = "https://api.vazifalar.uz"
let envName = "prod"
#elseif STAGING
let apiBaseURL = "https://staging.api.vazifalar.uz"
let envName = "staging"
#else
let apiBaseURL = "http://localhost:8080"
let envName = "dev"
#endif

print("env=\(envName) url=\(apiBaseURL)")

Endi BUILD faylida defines atributini select() bilan to'ldiramiz:

# cli/BUILD.bazel
load("@rules_swift//swift:swift_binary.bzl", "swift_binary")

swift_binary(
    name = "envinfo",
    srcs = ["main.swift"],
    # select() build vaqtida api_env qiymatiga qarab bitta shoxni tanlaydi.
    # Swift defines - bu shunchaki identifikator (qiymatsiz), #if uchun.
    defines = select({
        "//config:is_staging": ["STAGING"],
        "//config:is_prod": ["PROD"],
        "//conditions:default": ["DEV"],
    }),
)

🧠 Esda tuting: Swift'da defines — bu C'dagidek nom=qiymat emas, balki shunchaki identifikator (STAGING). Har biri -D bilan oldiga qo'shilib, swiftc'ga uzatiladi va #if STAGING'da ishlatiladi (manba: rules_swift swift_library docs, defines atributi). Qiymatli konfiguratsiya (masalan URL'ning o'zini -D orqali uzatish) Swift'da ishlamaydi — shuning uchun #if bilan qiymatni kod ichida tanlaymiz.

//conditions:default — hech bir shart to'g'ri kelmaganda ishlaydigan standart shox (docs/configurable-attributes). Uni yozmasangiz va hech narsa mos kelmasa, build xato beradi (buni 10-bo'limda ko'ramiz).

Endi eng yoqimli qismi — uni ishga tushiramiz. Standart holatda (hech qanday bayroqsiz) //conditions:default ishlaydi:

bazel run //cli:envinfo
env=dev url=http://localhost:8080

:is_staging shartini yoqadigan bayroq bilan (buni 9-bo'limda sozlaymiz):

bazel run //cli:envinfo --config=staging
env=staging url=https://staging.api.vazifalar.uz
bazel run //cli:envinfo --config=prod
env=prod url=https://api.vazifalar.uz

**Bitta Swift fayl, bitta select() — uchta muhit.** Hech bir #if'ni qo'lda o'zgartirmadik, hech bir sxema sozlamasini bosmadik. Bayroqni o'zgartirdik — Bazel qolganini qildi.

select() qanday ishlaganini "ichidan" ko'rish

select() haqiqatan swiftc buyrug'ini o'zgartirganini isbotlaymiz. bazel aquery — har bir action'ning aniq buyruq qatorini ko'rsatadi. Standart (dev) holatda:

bazel aquery 'mnemonic("SwiftCompile", //cli:envinfo)' | grep -oE "\-D ?[A-Z]+"
-DDEBUG
-DDEV

Endi staging bayrog'i bilan:

bazel aquery 'mnemonic("SwiftCompile", //cli:envinfo)' --//config:api_env=staging | grep -oE "\-D ?[A-Z]+"
-DDEBUG
-DSTAGING

Mana mexanizm: standartda swiftc -DDEV oldi, stagingda -DSTAGING. select() defines atributini mos shoxga analiz fazasida hal qildi, rules_swift esa buni -D bayrog'i qilib swiftc'ga uzatdi. (-DDEBUG — bu rules_swift'ning fastbuild rejimida avtomatik qo'shadigan bayrog'i; -c opt bilan u yo'qoladi — buni 10-bo'limda ko'ramiz.)

⚠️ Diqqat: select() rule atributlarida ishlaydi, macro ichida emas. Rasmiy hujjat (docs/configurable-attributes) buni alohida ta'kidlaydi: macro loading fazasida, ya'ni bayroqlar ma'lum bo'lishidan oldin ishlaydi — shuning uchun macro select()ning qaysi shoxini tanlashini bila olmaydi. Macro select()ni faqat "qora quti" sifatida rule'ga uzata oladi, lekin uning ichini ko'ra olmaydi. Agar macro ichida select qiymatini .upper() qilmoqchi bo'lsangiz, type 'select' has no method upper() xatosini olasiz.

select() uchta shoxdan bittasini config_setting shartiga qarab analiz fazasida tanlaydi; tanlangan qiymat swiftc buyrug'iga -D bayrog'i bo'lib kiradi va action key'ga ta'sir qiladi

Rasm select() oqimini ko'rsatadi: yuqorida uchta shox (dev/staging/prod), o'rtada --//config:api_env bayrog'i qaysi config_settingni yoqishi, pastda tanlangan qiymat swiftc buyrug'iga va undan action key'ga qanday o'tishi.

9. config_setting va o'z build setting'laringiz {#build-settings}

8-bo'limda //config:is_staging degan shartni ishlatdik, lekin uni hali ta'riflamadik. Endi shuni quramiz. Ikki qism kerak:

  1. Build setting — api_env degan loyihaga xos "sozlama". U dev/staging/prod qiymatlardan birini oladi.
  2. **config_setting** — "api_env stagingga teng bo'lsa, shu shart to'g'ri keladi" degan ko'prik.

--cpu yoki --compilation_mode kabi bayroqlar Bazel'ning ichiga o'rnatilgan (built-in). Lekin bizning api_env — loyihaga xos. Buni Bazel'ni o'zgartirmasdan, .bzl orqali e'lon qilamiz. Eng oson yo'l — bazel_skylib'ning tayyor string_flag rule'i (manba: extending/config → "Predefined settings"):

# config/BUILD.bazel
load("@bazel_skylib//rules:common_settings.bzl", "string_flag")

# Loyihaga xos build setting: qaysi muhit uchun quramiz.
# flag = command line'dan o'zgartirsa bo'ladigan setting.
string_flag(
    name = "api_env",
    build_setting_default = "dev",
    values = [
        "dev",
        "staging",
        "prod",
    ],
    visibility = ["//visibility:public"],
)

# Har bir muhit uchun bitta config_setting: api_env qiymatiga qarab mos keladi.
config_setting(
    name = "is_staging",
    flag_values = {":api_env": "staging"},
    visibility = ["//visibility:public"],
)

config_setting(
    name = "is_prod",
    flag_values = {":api_env": "prod"},
    visibility = ["//visibility:public"],
)

Bu yerda:

  • **string_flag** — @bazel_skylib//rules:common_settings.bzl'dan keladi. flag = True ma'nosini beradi (string_flag nomidagi flag), ya'ni uni command line'dan --//config:api_env=staging deb o'zgartirsa bo'ladi. values = [...] — ruxsat etilgan qiymatlar; boshqa qiymat bersangiz, Bazel xato beradi.
  • **config_setting** — flag_values orqali bizning build setting'imizga ulanadi: is_staging shartining ma'nosi "api_env aynan staging" (manba: docs/configurable-attributes → "Custom flags").

Bu uch qismning select() bilan birikishi: swift_binary'ning defines = select({"//config:is_staging": [...], ...}) ifodasi, foydalanuvchi --//config:api_env=staging berganda, is_staging to'g'ri kelib, STAGING shoxini tanlaydi.

Command line'ni qisqartirish: --config

--//config:api_env=staging — uzun. .bazelrc'da uni qisqa --config=staging ortiga yashiramiz (manba: run/bazelrc; .bazelrc bilan Bazel 2da tanishganmiz):

# .bazelrc
# Muhit config'lari: --config=staging / --config=prod
build:staging --//config:api_env=staging
build:prod --//config:api_env=prod

Shundan keyin bazel build //... --config=staging yozsangiz, Bazel avtomatik --//config:api_env=staging qo'shadi. Ikkala yo'l ham ishlaydi:

bazel run //cli:envinfo --//config:api_env=staging   # to'g'ridan-to'g'ri
bazel run //cli:envinfo --config=staging             # .bazelrc orqali qisqa

Real-world: rules_apple ham aynan shu naqshni ishlatadi

Bu "o'yinchoq" misol emas — aynan shu mexanizm rules_apple'ning o'zida ishlaydi. Oldingi darslarda simulyatorni .bazelrc'da shunday tanlaganmiz:

build --@rules_apple//apple/build_settings:ios_simulator_device="iPhone 17 Pro"
build --@rules_apple//apple/build_settings:ios_simulator_version="27.0"

ios_simulator_device va ios_simulator_version — bular aynan string_flag build setting'lar. rules_apple ularni o'zining apple/build_settings/BUILD faylida, xuddi biz yuqorida qilganimizdek, bazel_skylib'ning string_flag/bool_flag rule'lari bilan e'lon qiladi (manba: rules_apple apple/build_settings/build_settings.bzl va .../BUILD). Masalan ios_simulator_devicening standart qiymati "". Yana bir misol — apple_support 2.10.0 Xcode versiyasini select() orqali solishtiradigan mexanizm qo'shgan (xcode/xcode_version_at_least.bzl; manba: apple_support release notes, 2026). Ya'ni siz shu darsda o'rgangan string_flag + config_setting + select() — bu rasmiy iOS rule'lari ichida kundalik ishlatiladigan asbob.

📊 Real raqamlar: rules_apple 5.3.0'da simulyator tanlash tartibi (manba: refs skeptic-tekshiruvi, rules_apple manbasi): (1) test target atributlari device_type/os_version → (2) ios_simulator_device/ios_simulator_version build setting'lar → (3) native Bazel bayroqlari --ios_simulator_device/--ios_simulator_version → (4) hech biri yo'q bo'lsa, qattiq kodlangan "iPhone 15" (Xcode 27'da ishlamaydi). Shuning uchun biz build-setting shaklini ishlatamiz — u kelajakka chidamli.

💡 Pro tip: bool_flag ham bor (@bazel_skylib//rules:common_settings.bzl). Masalan --//config:enable_logging kabi true/false sozlama. Command line'da maxsus sintaksis: --//config:enable_logging (yoqish) va --no//config:enable_logging (o'chirish) (manba: extending/config).

10. --define, -c opt, platforms — konfiguratsiya xaritasi {#other-config}

string_flag/config_settingdan tashqari yana bir nechta konfiguratsiya vositasi bor. Qisqa xarita:

**--define (eski usul).** --define api_env=staging ko'rinishidagi eski sintaksis. config_settingda values = {"define": "api_env=staging"} yoki define_values = {"api_env": "staging"} bilan tutiladi. Rasmiy hujjat aniq aytadi (docs/configurable-attributes): "--define faqat orqaga moslik uchun qo'llab-quvvatlanadi. Iloji bo'lsa, Starlark build setting'lardan foydalaning." Ya'ni yangi kodda string_flag yozing, --define emas.

**-c opt / -c dbg / -c fastbuild (compilation mode).** Bu — built-in bayroq. config_settingda values = {"compilation_mode": "opt"} bilan tutiladi. Buni amalda ko'ramiz: 8-bo'limda swiftc fastbuildda -DDEBUG olgandi. -c opt bilan quradigan bo'lsak:

bazel aquery 'mnemonic("SwiftCompile", //cli:envinfo)' | grep -o "\-DDEBUG"
# -> -DDEBUG   (fastbuild)

bazel aquery 'mnemonic("SwiftCompile", //cli:envinfo)' -c opt | grep -o "\-DDEBUG"
# -> (hech narsa; -c opt da -DDEBUG yo'q)

-DDEBUG rejimga qarab paydo bo'lib yo'qoladi. Demak siz defines = select({"//conditions:default": [...]}) bilan, masalan, config_setting(values = {"compilation_mode": "dbg"}) orqali Debug/Release uchun turli qiymat bera olasiz. Rejimlar bilan iOS Debug/Release build'i Bazel 5da batafsil.

Platforms va constraints. Katta loyihalarda har bir bayroqni alohida berish o'rniga ularni to'plam (platform) qilib bog'laysiz. constraint_setting — "enum tipi", constraint_value — "enum qiymati", platform esa ularning to'plami (manba: docs/configurable-attributes → Platforms). select() constraint_valuelarni to'g'ridan-to'g'ri o'qiy oladi:

# misol: OS'ga qarab tanlash (bazel docs'dan moslashtirilgan)
swift_library(
    name = "Net",
    srcs = ["Net.swift"],
    defines = select({
        "@platforms//os:ios": ["PLATFORM_IOS"],
        "@platforms//os:macos": ["PLATFORM_MAC"],
        "//conditions:default": [],
    }),
)

iOS kontekstida platforms odatda rules_apple ichida avtomatik sozlanadi (--ios_multi_cpus=sim_arm64 kabi bayroqlar orqali), shuning uchun kundalik ishda ko'pincha string_flag + config_setting yetarli. Platforms'ning to'liq mexanizmi Bazel 5 va Bazel 17da.

⚠️ Diqqat: select()da to'g'ridan-to'g'ri platform'ni tekshirmang (":my_platform": [...]). Rasmiy hujjat buni taqiqlaydi, chunki semantikasi noaniq: platform ikkita constraint'ga ega bo'lsa-yu, aynan sizning platform'ingiz bo'lmasa, qaysi shox tanlanishi tushunarsiz. O'rniga config_setting(constraint_values = [...]) yozing — u "shu constraint'larga ega har qanday platform"ni anglatadi (manba: docs/configurable-attributes → FAQ).

11. Konfiguratsiya action key'ga qanday ta'sir qiladi {#action-key}

select() nega to'g'ri ishlaydi — ya'ni staging build'i dev build'ining cache'ini "ifloslantirmaydi"? Javob — action keyda. Bu mavzuni Bazel 10: Cacheda to'liq ochamiz, lekin bu yerda ko'prik tashlaymiz, chunki konfiguratsiya aynan shu joyda ishlaydi.

Har bir action (masalan "main.swift'ni swiftc bilan kompilyatsiya qil") uchun Bazel bir action key hisoblaydi. Unga kiradigan narsalar (manba: remote/caching): to'liq buyruq qatori, muhit o'zgaruvchilari, barcha kirish fayllarining content-hash'i, va konfiguratsiya.

select() qaysi shoxni tanlasa, buyruq qatori o'zgaradi: devda -DDEV, stagingda -DSTAGING. Buyruq qatori o'zgargani uchun action key ham o'zgaradi. Natijada:

  • dev build va staging build — ikki xil action key → ikki xil cache yozuvi. Ular bir-birini buzmaydi.
  • Siz devdan stagingga, keyin yana devga qaytsangiz — dev natijasi hali ham cache'da turibdi, Bazel uni qayta hisoblamaydi.

Buni cquery bilan ko'rsa bo'ladi. query loading fazasida ishlab, select()ning qaysi shoxini tanlashni bilmaydi (barcha shoxlarni ko'rsatadi). cquery esa analiz fazasidan keyin ishlaydi — u konfiguratsiyani biladi:

bazel cquery //cli:envinfo
//cli:envinfo (71ac6bd)

Qavs ichidagi (71ac6bd) — bu konfiguratsiya hash'i. --config=staging bilan boshqa hash chiqadi, chunki konfiguratsiya boshqacha. Aynan shu hash action key'ga kiradi va select()ning cache'dagi ajralishini ta'minlaydi.

🧠 Esda tuting: "Build = e'lon qilingan kirishlarning sof funksiyasi" degan Bazel printsipini (Bazel 1dan) eslang. Konfiguratsiya — o'sha "kirish"ning bir qismi. select() qiymatni o'zgartiradi → kirish o'zgaradi → action key o'zgaradi → to'g'ri, mustaqil cache. Shuning uchun Bazel'da "Debug build Release cache'ni buzib qo'ydi" degan muammo yo'q.

12. Starlark'ni debug qilish: print(), fail(), buildifier {#debugging}

Starlark kodingiz xato qilsa, uchta asosiy quroldan foydalanasiz.

print() — loading fazasidagi log

print() xabarni loading fazasida DEBUG log qatori sifatida chiqaradi (3-bo'limdagi demo'da ko'rdik). U faqat debug uchun. .bzl style guide ogohlantiradi (rules/bzl-style): "Ishlab chiqarish (production) kodida print() ishlatmang; u sizning .bzl faylingizning barcha foydalanuvchilarini spam qiladi." Agar macro'da vaqtincha debug kerak bo'lsa, uni if DEBUG: (standart False) ortiga yashiring va topshirishdan oldin olib tashlang.

fail() — qattiq xato

fail() build'ni darhol to'xtatadi va xabar chiqaradi. Bu Starlark'ning raise/exceptsiz yagona xato mexanizmi — xatoni "ushlab" davom etib bo'lmaydi (manba: extending/legacy-macros → Errors). Biz ios_moduleda fail()ni validatsiya uchun ishlatdik: srcs bo'sh bo'lsa, foydalanuvchiga tushunarli xabar beramiz.

Keling, uni ishga tushiramiz — srcs = [] bilan chaqiramiz:

# scratch_fail/BUILD.bazel
load("//tools:defs.bzl", "ios_module")

ios_module(
    name = "Broken",
    srcs = [],
)
bazel query //scratch_fail:all
ERROR: .../scratch_fail/BUILD.bazel:3:11, in <toplevel>
    fail("ios_module(%s): 'srcs' bo'sh bo'lishi mumkin emas" % name)
Error in fail: ios_module(Broken): 'srcs' bo'sh bo'lishi mumkin emas

Xato aniq, loyihaga xos va foydalanuvchini to'g'ri joyga yo'naltiradi. Yaxshi fail() xabari — "nima noto'g'ri va qanday tuzatish kerak"ni aytadi.

select()ning "mos kelmadi" xatosi

select()da //conditions:default yozmasangiz va hech bir shart to'g'ri kelmasa, Bazel aniq xato beradi. Buni ko'rish uchun standart shoxsiz select() yozamiz (api_env standart qiymati dev, lekin faqat is_staging/is_prod shoxlari bor):

bazel build //scratch_nomatch:nomatch
ERROR: .../scratch_nomatch/BUILD.bazel:3:13: configurable attribute "defines"
in //scratch_nomatch:nomatch doesn't match this configuration. Would a default
condition help?
Conditions checked:
 //config:is_staging
 //config:is_prod

Bazel hatto yechimni ham taklif qilyapti ("Would a default condition help?") va qaysi shartlarni tekshirganini sanaydi. Agar qasddan faqat ma'lum muhitlarni qo'llab-quvvatlasangiz, bu xato foydali — kutilmagan sozlamadan himoya qiladi. Aks holda //conditions:default qo'shing.

buildifier — formatlash va lint

buildifier (brew install buildifier) — BUILD/.bzl fayllari uchun rasmiy formatter va linter (rules/bzl-styleda tavsiya etilgan). Avval formatlash. Mana tartibsiz fayl:

# messy_BUILD.bazel (formatlashdan oldin)
load("@rules_swift//swift:swift_library.bzl","swift_library")
swift_library(name="Core",srcs=["a.swift"],deps=[":Zebra",":Apple",":Mango"])
buildifier messy_BUILD.bazel
# formatlashdan keyin
load("@rules_swift//swift:swift_library.bzl", "swift_library")

swift_library(name = "Core", srcs = ["a.swift"], deps = [":Zebra", ":Apple", ":Mango"])

E'tibor bering: = atrofida va vergullardan keyin bo'sh joy qo'yildi, load'dan keyin bo'sh qator qo'shildi. buildifier load() ichidagi simvollarni va fayldagi load operatorlarini ham alifbo tartibida saralaydi:

# oldin
load("@rules_swift//swift:swift_library.bzl", "swift_library")
load("@bazel_skylib//rules:common_settings.bzl", "string_flag", "bool_flag")
buildifier loadsort.bzl
# keyin: load statement'lar va ularning ichidagi simvollar saralandi
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag", "string_flag")
load("@rules_swift//swift:swift_library.bzl", "swift_library")

Lint rejimi (--lint=warn) uslub ogohlantirishlarini beradi:

buildifier --lint=warn --mode=check messy.bzl
messy.bzl:1: module-docstring: The file has no module docstring.
A module docstring is a string literal (not a comment) which should be the
first statement of a file (it may follow comment lines).

💡 Pro tip: buildifierni CI'da buildifier --mode=check --lint=warn bilan ishlating — formatlanmagan yoki lint ogohlantirishi bor fayl PR'ni yiqitsin. Shunda BUILD fayllaringiz butun jamoada bir xil ko'rinishda qoladi. CI sozlamasi Bazel 11da.

Amaliy mashqlar {#mashqlar}

Mashqlarni shu darsdagi starlark_ws workspace'ida bajaring (cli, config, Feature, tools, lang paketlari bilan). Har biridan keyin natijani bazel run/bazel query/bazel build bilan tekshiring — kodni ko'z bilan tekshirish yetarli emas.

  1. **bool_flag bilan logging.** config/BUILD.bazel'ga bazel_skylib'dan bool_flag yuklab, enable_logging nomli setting qo'shing (standart False) va unga mos config_setting yozing. cli'ning definesiga ikkinchi select() qo'shib, enable_logging yoqilganda LOGGING define'i kiradigan qiling (ikkita select()ni + bilan qo'shish mumkin — docs/configurable-attributes → "Combining selects"). main.swift'da #if LOGGING bilan qo'shimcha qator chiqaring. Tekshiring: bazel run //cli:envinfo --no//config:enable_logging va --//config:enable_logging.
  2. **ios_module'ni mustahkamlang.** Macro'ga fail() tekshiruvi qo'shing: minimum_os_version "15.0"dan past bo'lsa, tushunarli xato bersin. (Maslahat: string'ni solishtirishdan ehtiyot bo'ling — oddiy < string'lar uchun leksikografik; minimum_os_version qiymatini ruxsat etilganlar ro'yxati bilan solishtiring yoki .split(".") qiling.)
  3. Modul fabrikasi (comprehension). Yangi .bzl faylda legacy macro yozing: many_libs(prefix, count) — for i in range(count) orqali count ta swift_library yaratsin (name = "%s%d" % (prefix, i)). BUILD'dan chaqiring va bazel query //pkg:all bilan hammasi yaratilganini ko'ring. Bu — comprehension/loop faqat .bzl (yoki legacy macro) ichida ishlashining amaliy isboti.
  4. Macro yaratgan target'larni toping. bazel query --output=build 'attr(generator_function, ios_module, //...)' ni ishga tushiring. ios_module yaratgan barcha target'lar ro'yxatini oling. Bu katta kod bazasida "bu target qayerdan keldi?" degan savolga javob beradigan asosiy texnika.
  5. **⭐ selectni macro ichida "ushlashga" urining.** Bir legacy macro yozing, u parametr sifatida kelgan defines qiymatini [d.upper() for d in defines] qilib o'zgartirmoqchi bo'lsin, va BUILD'da unga select({...}) bering. Build qiling va type 'select' has no method ... xatosini o'qing. Keyin macro'ni selectni shunchaki rule'ga uzatadigan qilib tuzating. Bu mashq "nega select() macro'da ishlamaydi"ni bir umrga esda qoldiradi.
  6. **⭐ cquery bilan konfiguratsiyani kuzating.** bazel cquery //cli:envinfo va bazel cquery //cli:envinfo --config=staging ni solishtiring — qavs ichidagi konfiguratsiya hash'i o'zgarishini ko'ring. Keyin bazel config <hash> bilan o'sha konfiguratsiya ichidagi qiymatlarni oching (docs/configurable-attributes → "Why doesn't my select() choose what I expect?").

O'zingizni tekshiring {#checklist}

  • [ ] Starlark'ning Python'dan farq qiladigan kamida 5 ta joyini (while yo'q, rekursiya yo'q, class yo'q, import o'rniga load(), muzlash) va nega shundayligini ayta olaman.
  • [ ] BUILD faylida nima qilib bo'lmasligini (funksiya e'loni, top-level for/if) va buni qanday xato chiqishini bilaman.
  • [ ] load() sintaksisini, _ bilan boshlanadigan simvol eksport qilinmasligini tushunaman.
  • [ ] Legacy macro va symbolic macro farqini (nom sxemasi, tipli atributlar, visibility himoyasi) ayta olaman.
  • [ ] ios_module kabi symbolic macro yoza olaman: implementation + attrs, name/visibility parametrlari, nom sxemasi (name + "_...").
  • [ ] bazel query --output=build bilan macro ochilishini va generator_functionni ko'ra olaman.
  • [ ] select() + config_setting + string_flag bilan bitta kodni dev/staging/prod uchun quradigan qilib sozlay olaman.
  • [ ] Nega select() macro'da emas, rule atributida ishlashini va u action key'ga qanday ta'sir qilishini tushunaman.
  • [ ] print(), fail() va buildifier bilan Starlark kodini debug/lint qila olaman.

Xulosa {#xulosa}

  • Starlark — Python emas, balki ataylab qisqartirilgan konfiguratsiya tili. while/rekursiya/class/import yo'q, qiymatlar load'dan keyin muzlaydi — hammasi determinizm, hermetiklik va parallel xavfsizlik uchun.
  • **BUILD'da nima quriladi (deklarativ target'lar), .bzl'da qanday quriladi (funksiya, macro, rule, konstanta).** Ular orasidagi yagona ko'prik — load().
  • Macro takrorlanishni yo'qotadi. Symbolic macro (Bazel 8+) tipli atributlar, nom sxemasi va visibility himoyasini beradi; legacy macro moslashuvchan, lekin "action at a distance"ga yo'l qo'yadi. Yangi kodda symbolic macro'ni tanlang.
  • **Bitta ios_module chaqiruvi → swift_library + test kutubxona + ios_unit_test.** Biz uni qurdik, query --output=build bilan ochilishini ko'rdik va test'ni simulyatorda PASSED qilib ishlatdik.
  • **select() — build vaqtidagi tanlov.** string_flag + config_setting bilan bitta Swift CLI'ni dev/staging/prod uchun qurdik; aquery swiftcga -DDEV yoki -DSTAGING o'tganini ko'rsatdi. Aynan shu naqsh rules_apple va apple_support ichida ishlatiladi.
  • Konfiguratsiya action key'ning bir qismi — shuning uchun turli muhit build'lari bir-birining cache'ini buzmaydi (to'liq Bazel 10da).
  • Debug uch quroli: print() (faqat vaqtincha), fail() (tushunarli xato), buildifier (format + lint, CI'da majburiy).
  • Keyingi dars: Bazel 5: Birinchi iOS ilova — rules_apple, rules_swift, SwiftUI, Info.plist va simulator

Manbalar {#manbalar}

  1. Starlark Language — Bazel docs — tiplar, Python'dan farqlar, muzlash (immutability).
  2. BUILD files — Bazel docs — BUILD vs .bzl, load(), hermetiklik.
  3. Macros (symbolic) — Bazel docs — macro(), attrs, nom sxemasi, visibility.
  4. Legacy Macros — Bazel docs — eski usul, native., fail(), label hal qilish.
  5. Creating a Symbolic Macro (tutorial) — Bazel docs — implementation + inherit_attrs naqshi.
  6. Configurable Build Attributes (select) — Bazel docs — select(), config_setting, default shart, nega macro'da ishlamaydi.
  7. Configurations / build settings — Bazel docs — string_flag, bool_flag, --flag_alias, label-typed setting'lar.
  8. .bzl style guide — Bazel docs — macro konvensiyalari, print() ogohlantirishi, DRY.
  9. Sharing Variables — Bazel docs — BUILD'lar orasida konstanta ulashish.
  10. bazel-skylib common_settings.bzl — string_flag/bool_flag manbasi.
  11. rules_swift — swift_library atributlari — defines, copts, local_defines semantikasi.
  12. rules_apple — apple/build_settings/build_settings.bzl — ios_simulator_device/_version real build setting'lari.
  13. Bazel 9.0 blogi — WORKSPACE olib tashlandi, objc_library/cc_* endi @rules_cc'dan yuklanadi, --incompatible_autoload_externally bo'sh.
  14. apple_support releases — 2.10.x, Xcode versiyasini select() bilan solishtirish.

Markdown manba

iOS Lessons