Telegram-iOS Custom Xcode Project Generation: How the MakeProject Build System Works

Telegram-iOS generates its Xcode project using a custom MakeProject workflow that orchestrates Bazel and Tulsi through the build-system/generate-xcode-project.sh script, automating environment validation, Tulsi compilation, Bazel configuration injection, and simulator-specific patches.

Unlike typical iOS projects that commit .xcodeproj files to version control, the Telegram-iOS repository dynamically generates its Xcode project from a Bazel build graph. This custom Xcode project generation pipeline ensures the IDE stays synchronized with the declarative build rules defined in the repository.

The MakeProject Architecture: Bazel and Tulsi

The foundation of Telegram-iOS's build system lies in two tools: Bazel for deterministic builds and Tulsi (Google's Xcode-Bazel bridge) for IDE integration. Rather than using standard Xcode project templates, the repository ships with a shell script that compiles Tulsi from source, configures it with project-specific Bazel flags, and emits a fully configured .xcodeproj file.

This workflow lives in build-system/generate-xcode-project.sh and executes a ten-step pipeline when invoked with an app target name (e.g., Telegram).

Step-by-Step: How the Project Generation Script Works

Step 1: Verify Environment

The script first validates that the correct bazel binary exists in PATH (using bazel_x86_64 on Apple Silicon if needed). It reads the required Xcode version from build-system/xcode_version and compares it against the active developer directory, aborting if they mismatch.

Step 2: Prepare Output Folders

The generator creates build-input/gen/project and cleans up previous artifacts. It removes any existing *.tulsiproj directories and the temporary Tulsi.app bundle to prevent stale configuration conflicts.

Step 3: Build Tulsi

Using Bazel, the script builds the Tulsi binary from the target //:tulsi defined in build-system/tulsi/BUILD.bazel. This produces bazel-bin/tulsi.zip containing the Tulsi application bundle.

Step 4: Unpack Tulsi

The script extracts tulsi.zip into build-input/gen/project/Tulsi.app, making the Tulsi binary available for project generation commands.

Step 5: Assemble Bazel Options

The script detects the number of logical CPUs and constructs an array of Bazel options optimized for Swift compilation:

BAZEL_OPTIONS=(
    --features=swift.use_global_module_cache
    --spawn_strategy=standalone
    --strategy=SwiftCompile=standalone
    --features=swift.enable_batch_mode
    --swiftcopt=-j${CORE_COUNT_MINUS_ONE}
)

If environment variables define remote or disk cache URLs, the script appends these flags to enable distributed caching for IDE builds.

Step 6: Create the Tulsi Project

The script invokes the Tulsi binary to generate a *.tulsiproj configuration file for the specified app target. This file points to the workspace root, the Bazel binary location, and the designated output folder.

Step 7: Patch the Tulsi Project

Before generating the Xcode project, the script modifies the *.tulsigen file to inject the Bazel options from Step 5 into both Debug and Release configurations. It also expands the sourceFilters list to ensure Tulsi includes the app target, submodules, and third-party sources in the generated IDE project.

Step 8: Generate the Xcode Project

With the patched configuration, the script calls Tulsi again using the --genconfig flag to emit the final .xcodeproj into the generated folder. This step runs non-interactively and does not automatically open Xcode.

Step 9: Post-Process the Generated Project

The generator applies two critical patches to bazel_build_settings.py inside the generated Xcode project:

  1. Environment access: Inserts import os at the top of the script so it can read environment variables during builds.
  2. Simulator architecture: Modifies the --cpu flag logic to use ios_sim_arm64 when building for the iPhone Simulator on Apple Silicon, preventing architecture mismatches.

Step 10: Open the Project

Finally, the script launches Xcode with the newly generated project, completing the custom Xcode project generation workflow.

Bazel Configuration Injection

The generator ensures that IDE builds use the same optimization flags as command-line builds. In build-system/generate-xcode-project.sh (lines 80–86), the script uses sed to inject the assembled Bazel flags into the Tulsi-generated configuration:

sed -i "" -e '1h;2,$H;$!d;g' -e 's/\('"${NAME}"' : {\n[ ]*"p" : "$(inherited)\)/\1'" ${BAZEL_OPTIONS[*]}"'/' \
    "$GEN_DIRECTORY/${APP_TARGET}.tulsiproj/Configs/${APP_TARGET}.tulsigen"

This ensures Swift module caching and parallel compilation settings propagate from the Bazel configuration to Xcode's build system.

Patching for Apple Silicon Simulators

To support Apple Silicon Macs, the script patches bazel_build_settings.py (located at build-input/gen/project/*.xcodeproj/.tulsi/Scripts/bazel_build_settings.py) with dynamic CPU selection logic:

sed -i '' -e '1h;2,$H;$!d;g' \
    -e "s/'--cpu=ios_arm64'/'--cpu=ios_arm64'.replace('ios_arm64', 'ios_sim_arm64' if os.environ.get('EFFECTIVE_PLATFORM_NAME') == '-iphonesimulator' else 'ios_arm64')/g" \
    "$GEN_DIRECTORY/${APP_TARGET}.xcodeproj/.tulsi/Scripts/bazel_build_settings.py"

This substitution checks the EFFECTIVE_PLATFORM_NAME environment variable at build time, automatically switching to the simulator architecture when Xcode targets the iOS Simulator.

Running the Generator

To generate an Xcode project for the Telegram app target, run the following from the repository root:

sh build-system/generate-xcode-project.sh Telegram

The script enforces strict error handling with set -e, aborting immediately if any step fails. Verify that your Xcode version matches the requirement specified in build-system/xcode_version before executing.

Key Files in the Build System

  • build-system/generate-xcode-project.sh: The central orchestration script that drives the entire custom Xcode project generation pipeline.
  • build-system/tulsi/BUILD.bazel: Contains the Bazel target //:tulsi used to build the Tulsi binary from source.
  • build-system/xcode_version: A version lock file specifying the exact Xcode release required by the repository.
  • build-input/gen/project/*.xcodeproj/.tulsi/Scripts/bazel_build_settings.py: Generated script invoked by Xcode during builds; patched by the generator for environment variable access and architecture switching.

Summary

  • Telegram-iOS uses a custom MakeProject workflow instead of static Xcode project files to maintain synchronization with its Bazel build graph.
  • The build-system/generate-xcode-project.sh script automates a ten-step process including environment validation, Tulsi compilation, Bazel option injection, and post-generation patching.
  • Tulsi bridges the gap between Bazel and Xcode, but requires extensive patching to support Swift module caching and Apple Silicon simulators.
  • Generated projects are ephemeral and should not be committed to version control; they are recreated on demand using the shell script.

Frequently Asked Questions

Why does Telegram-iOS generate Xcode projects instead of including them in the repository?

Static Xcode project files quickly become desynchronized with Bazel build rules. By generating the project dynamically using build-system/generate-xcode-project.sh, Telegram-iOS ensures that IDE builds use exactly the same compiler flags, source paths, and dependencies defined in the Bazel workspace. This eliminates the "works in Xcode but not in CI" class of errors.

What is Tulsi and why does Telegram-iOS build it from source?

Tulsi is Google's open-source tool that generates Xcode projects from Bazel BUILD files. Telegram-iOS builds Tulsi from the //:tulsi target in build-system/tulsi/BUILD.bazel rather than using a prebuilt binary to ensure compatibility with the specific Bazel version and custom patches used in the repository. Building from source also allows the MakeProject system to apply Telegram-specific modifications during the generation process.

How does the script handle Apple Silicon Macs differently?

The script detects Apple Silicon hardware and uses the bazel_x86_64 binary for Tulsi compilation to maintain compatibility. It also patches bazel_build_settings.py to switch the --cpu flag from ios_arm64 to ios_sim_arm64 when the EFFECTIVE_PLATFORM_NAME environment variable indicates an iPhone Simulator target, ensuring simulator builds run natively on ARM64 Macs without Rosetta translation.

Can I use custom Bazel flags when generating the Xcode project?

Yes. The generate-xcode-project.sh script checks for environment variables defining remote or disk cache URLs and appends them to the BAZEL_OPTIONS array. You can also modify the script directly to inject additional flags into the generated .tulsigen configuration before Step 8 executes, though this requires editing build-system/generate-xcode-project.sh lines 55–70.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →