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

> Discover how Telegram-iOS automates Xcode project generation with the MakeProject build system, orchestrating Bazel and Tulsi for seamless development.

- Repository: [TelegramMessenger/Telegram-iOS](https://github.com/TelegramMessenger/Telegram-iOS)
- Tags: internals
- Published: 2026-04-07

---

**Telegram-iOS generates its Xcode project using a custom MakeProject workflow that orchestrates Bazel and Tulsi through the [`build-system/generate-xcode-project.sh`](https://github.com/TelegramMessenger/Telegram-iOS/blob/main/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`](https://github.com/TelegramMessenger/Telegram-iOS/blob/main/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:

```bash
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`](https://github.com/TelegramMessenger/Telegram-iOS/blob/main/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`](https://github.com/TelegramMessenger/Telegram-iOS/blob/main/build-system/generate-xcode-project.sh) (lines 80–86), the script uses `sed` to inject the assembled Bazel flags into the Tulsi-generated configuration:

```bash
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`](https://github.com/TelegramMessenger/Telegram-iOS/blob/main/bazel_build_settings.py) (located at `build-input/gen/project/*.xcodeproj/.tulsi/Scripts/bazel_build_settings.py`) with dynamic CPU selection logic:

```bash
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:

```bash
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`](https://github.com/TelegramMessenger/Telegram-iOS/blob/main/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`](https://github.com/TelegramMessenger/Telegram-iOS/blob/main/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`](https://github.com/TelegramMessenger/Telegram-iOS/blob/main/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`](https://github.com/TelegramMessenger/Telegram-iOS/blob/main/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`](https://github.com/TelegramMessenger/Telegram-iOS/blob/main/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`](https://github.com/TelegramMessenger/Telegram-iOS/blob/main/build-system/generate-xcode-project.sh) lines 55–70.