How Telegram-iOS Uses Bazel for Build System Architecture and Dependency Management
Telegram-iOS employs Bazel to create a fully reproducible, incremental build system that orchestrates hundreds of third-party libraries and internal modules across iOS, macOS, and Apple simulator targets through declarative BUILD files and external dependency resolution.
The TelegramMessenger/Telegram-iOS repository is built entirely with Bazel, replacing traditional Xcode project management with a hermetic, scalable build architecture. This approach enables deterministic builds across different development environments while supporting complex dependency graphs spanning native C libraries, Swift components, and Apple-specific resource bundles.
Core Bazel Configuration Files
The root of the repository contains several critical files that define the build environment and external dependencies.
MODULE.bazel and External Rule Dependencies
The MODULE.bazel file serves as the central dependency declaration, importing essential rulesets including rules_apple, rules_swift, bazel_features, and the Apple provisioning-profile extension. It also fetches versioned binary archives for CMake, Meson, Ninja, and FlatBuffers via http_file rules with SHA-256 checksums to guarantee reproducibility.
Global Build Flags in .bazelrc
The .bazelrc file configures global Bazel behavior for Apple platforms. It selects the Apple crosstool, enables per-file compiler options for Objective-C (-fno-objc-msgsend-selector-stubs), activates Swift caching mechanisms (swift.cacheable_swiftmodules), and forces standalone workers for genrules and Swift compilation to minimize contention during parallel builds.
Root BUILD.bazel and SourceKit Support
The BUILD.bazel at the repository root exposes a setup_sourcekit_bsp target that enables IDE indexing across Swift, Objective-C, and C++ sources. It also exports the versions.json file consumed by CI pipelines.
Bootstrap Scripting with prepare-build.sh
Before any Bazel command runs, the build-system/prepare-build.sh script orchestrates the build environment. It creates the build-input/data package, copies provisioning profiles via copy-provisioning-profiles-*.sh scripts, and generates build variables from template_minimal_development_configuration.json. This JSON template supplies app-specific values including bundle identifiers and API keys required by the prepare-build-variables-*.sh scripts.
Dependency Management Architecture
Telegram-iOS organizes its massive dependency graph into distinct categories, each handled through specific Bazel patterns.
Third-Party Libraries in third-party/
External dependencies reside under third-party/, where each library contains a BUILD file declaring cc_library, objc_library, or swift_library targets. These files leverage @build_bazel_rules_apple macros to apply platform-specific flags.
For example, third-party/libyuv/BUILD uses select() statements to choose architecture-specific compiler flags:
"@build_bazel_rules_apple//apple:ios_arm64": common_flags + arm64_specific_flags,
"@build_bazel_rules_apple//apple:ios_x86_64": common_flags + x86_64_specific_flags,
Similarly, third-party/openh264/BUILD demonstrates per-architecture source list selection:
"@build_bazel_rules_apple//apple:ios_arm64": arm64_specific_sources,
"@build_bazel_rules_apple//apple:ios_x86_64": [],
Internal Submodules Structure
Internal code lives in submodules/, with each module containing its own BUILD file that wires the component into the global dependency graph. These modules often reference resources through @build_bazel_rules_apple//apple:resources.bzl and declare visibility constraints controlling which top-level targets can link against them.
Versioned External Binaries
Build-time tools including CMake, Meson, Ninja, and FlatBuffers are fetched via http_file rules defined in MODULE.bazel. Their SHA-256 checksums ensure that every build uses identical binary versions, preventing "works on my machine" discrepancies.
Apple Provisioning Profile Handling
Rather than committing provisioning profiles to version control, Telegram-iOS uses the provisioning_profile_repository extension from rules_apple. The prepare-build.sh script injects the appropriate development or distribution profile based on script arguments, enabling seamless certificate management without repository bloat.
Build Flow and Execution
The standard development workflow follows a strict sequence:
- Developer executes
./build-system/prepare-build.sh Telegram development - The script populates
build-input/datawith provisioning profiles and generates JSON configuration - Bazel resolves the dependency graph, pulling external archives and compiling submodules
- The Apple toolchain (
@build_bazel_rules_apple//apple:ios.bzl) links the final//Telegram:Telegramtarget
This flow ensures that build variables and signing assets are validated before compilation begins, preventing late-stage failures during code signing.
Performance Optimization and Caching
The .bazelrc configuration forces standalone execution strategies for genrules and Swift compilation (--strategy=Genrule=standalone, --strategy=SwiftCompile=worker). This reduces worker contention and improves incremental build performance on macOS hosts.
Additionally, the swift.debug_prefix_map parameter ensures that debug symbols remain deterministic across different checkout paths, which is essential for CI reproducibility and remote caching.
IDE Support and SourceKit Integration
The setup_sourcekit_bsp rule in BUILD.bazel creates a SourceKit-BSP server that watches all source files in the repository. This provides real-time diagnostics and indexing to Xcode or other LSP-compatible editors, compensating for the fact that Telegram-iOS does not rely on checked-in .xcodeproj files.
Practical Implementation Examples
Integrating a New Third-Party C Library
To add a library like libfoo, declare the archive in MODULE.bazel:
http_file(
name = "libfoo_tar",
urls = ["https://example.com/libfoo-1.2.3.tar.gz"],
sha256 = "e3b0c44298fc1c149afbf4c8996fb924...",
)
Then create third-party/libfoo/BUILD with architecture-specific optimizations:
load("@rules_cc//cc:defs.bzl", "cc_library")
cc_library(
name = "foo",
srcs = glob(["src/**/*.c"]),
hdrs = glob(["include/**/*.h"]),
includes = ["include"],
visibility = ["//visibility:public"],
copts = select({
"@build_bazel_rules_apple//apple:ios_arm64": ["-march=armv8.2-a"],
"@build_bazel_rules_apple//apple:ios_x86_64": ["-march=x86-64"],
"//conditions:default": [],
}),
)
Other modules can then reference this library via deps = ["@libfoo//:foo"].
Creating an Internal Submodule
The submodules/ChatUI/BUILD file demonstrates how internal components declare dependencies:
objc_library(
name = "ChatUI",
srcs = glob(["**/*.m", "**/*.mm"]),
hdrs = glob(["**/*.h"]),
resources = glob(["Resources/**"]),
deps = [
"//submodules/AvatarNode:AvatarNode",
"//third-party/libyuv:libyuv",
],
visibility = ["//Telegram:Telegram"],
)
This configuration bundles Objective-C implementation, resources, and dependencies on both internal modules (AvatarNode) and third-party libraries (libyuv), with visibility restricted to the top-level Telegram target.
Summary
- Telegram-iOS uses Bazel as its exclusive build system, replacing Xcode project files with declarative
BUILDconfigurations. - The
MODULE.bazelfile manages external rulesets and binary dependencies viahttp_filewith SHA-256 verification. - Platform-specific compilation flags are selected using
@build_bazel_rules_appleselect()patterns inthird-party/BUILD files. - The
build-system/prepare-build.shscript bootstraps the environment by provisioning profiles and generating build variables before Bazel execution. - Performance optimizations in
.bazelrcinclude standalone workers, Swift module caching, and deterministic debug prefix mapping. - IDE support is provided through a SourceKit-BSP target defined in the root
BUILD.bazelfile.
Frequently Asked Questions
Why did Telegram-iOS choose Bazel over native Xcode builds?
According to the Telegram-iOS source code, Bazel provides reproducible, incremental builds across iOS, macOS, and simulator targets while managing hundreds of dependencies. Unlike Xcode projects, which require manual configuration of each target's build settings, Bazel's declarative approach ensures consistent compiler flags and dependency resolution across different developer machines and CI environments.
How are iOS provisioning profiles managed in the Bazel build?
Provisioning profiles are handled through the provisioning_profile_repository extension from rules_apple, as configured in MODULE.bazel. The build-system/prepare-build.sh script calls copy-provisioning-profiles-*.sh helpers to inject the correct development or distribution certificates into the build-input/data directory before compilation, keeping sensitive credentials out of version control.
Can I build the Telegram-iOS app without running prepare-build.sh?
No, the build requires the bootstrap script. The prepare-build.sh script generates essential configuration files including the JSON build variables derived from template_minimal_development_configuration.json and organizes provisioning profiles. Attempting to run bazel build directly will fail because these generated inputs are required by targets in the Telegram/BUILD file.
How does the build system handle architecture-specific compiler flags?
The repository uses Bazel's select() mechanism with conditions from @build_bazel_rules_apple, such as @build_bazel_rules_apple//apple:ios_arm64 and @build_bazel_rules_apple//apple:ios_x86_64. Libraries like libyuv and OpenH264 in the third-party/ directory demonstrate this pattern by applying different copts or source lists based on the target architecture, ensuring optimized binaries for each platform.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →