Using Bazel with your iOS Projects
Tags: ios, programming, tutorial, productivity
Published: 2023-06-23 23:51:11

When making a product, there comes a time when you need to work smarter, not harder. This is especially true when your app gets big and complicated. Just like Facebook shared in their [post](https://engineering.fb.com/2017/11/09/android/rethinking-android-app-compilation-with-buck/) (2017), it's wise to find better ways to manage the building of your app.

That's where Bazel comes in.

Bazel is a free tool from Google that helps build your apps. It's great for big, complex apps, but also works well for smaller ones. This article is all about how to use Bazel to make building your iOS apps quicker and easier.

We'll start with the basics: how to set up Bazel and use it for the first time. By the end of this article, you'll have a good understanding of how to use Bazel and why it can make your app building process better.

## Motivation

Take a look at these important companies using Bazel to build their apps, and in some cases, their backend code as well: https://bazel.build/community/users

Most of these companies have specialized infrastructure teams handling their build tools. Fortunately for us, these teams also contribute to Bazel's open-source code, allowing us to leverage the collective expertise of these professionals to our benefit.

## Installing Bazel

To start, visit https://github.com/bazelbuild/bazel/releases and select the version you wish to use. If you're new to Bazel, it's recommended to choose the most recent version that isn't labeled as a "pre-release".

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rttj3god7m40urwl08vg.png)

Download the file appropriate for your platform. If you're using an Apple Silicon-based computer, your file name will end with `-darwin-arm64`.

Next, move the downloaded file to a location within your `$PATH`, and check to confirm it's operational:

```
$ bazel --version
bazel *.*.*
```

For other installation methods (including using Homebrew), visit https://bazel.build/install/os-x

## Project Setup

We're now ready to create our first Bazel-based iOS app. Although we'll be using Xcode later, we need to manually set up some basic structure first since we're opting for an alternative build system.

To simplify the process, I've prepared a template: https://github.com/petertechstories/ios-bazel-template

Here's the structure of the project folder:

```
BUILD
Info.plist
[MyModule]
[Resources]
[Sources]
WORKSPACE
```

Let's unpack what's going on here.

Let's break down what each of these files and folders do.

`WORKSPACE` is the starting point of our project. It lists all the tools needed to build the app. The contents of this file might seem complex initially, but for the most part, it's a standard setup suitable for Apple platforms. A typical example can be found at https://github.com/bazelbuild/rules_apple/releases

`BUILD` provides a detailed description of the project. Think of it like an `.xcodeproj` file, but with simpler syntax that's easy for humans to read. For our template project, it's pretty straightforward:

```
load("@build_bazel_rules_apple//apple:ios.bzl", "ios_application")
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")

load(
    "@rules_xcodeproj//xcodeproj:defs.bzl",
    "top_level_targets",
    "xcodeproj",
    "xcode_provisioning_profile",
)

load("@build_bazel_rules_apple//apple:apple.bzl", "local_provisioning_profile")

swift_library(
    name = "Sources",
    srcs = [
        "Sources/AppDelegate.swift",
    ],
    data = [
        "//Resources:Main.storyboard",
    ],
    deps = [
        "//MyModule"
    ]
)

local_provisioning_profile(
    name = "xcode_managed_profile",
    profile_name = "iOS Team Provisioning Profile: com.test.BazelApp",
    tags = ["manual"],
)

ios_application(
    name = "App",
    app_icons = ["//Resources:PhoneAppIcon.xcassets"],
    bundle_id = "com.test.BazelApp",
    families = [
        "iphone",
        "ipad",
    ],
    provisioning_profile = ":xcode_managed_profile",
    infoplists = [":Info.plist"],
    launch_storyboard = "//Resources:Launch.storyboard",
    minimum_os_version = "13.0",
    deps = [":Sources"],
    visibility = ["//visibility:public"],
)

xcodeproj(
    name = "xcodeproj",
    project_name = "App",
    tags = ["manual"],
    top_level_targets = top_level_targets(
        labels = [
            ":App",
        ],
        target_environments = ["device", "simulator"],
    ),    
)
```

It might seem like we're looking at a series of Python function calls, and in a way, we are. Bazel uses a Turing-complete language to understand project descriptions. However, it's important to note that even though it's possible, it's not recommended to perform heavy computations in this space.

First, we `load` a few definitions from our toolset. In Bazel language, the components we import and use to define different parts of the project are called "rules". The `load` statements can only reference those rules that have been defined in the `WORKSPACE` file.

Next, we define a Swift module that will hold the app's code. This is done using the [swift_library](https://github.com/bazelbuild/rules_swift/blob/master/doc/rules.md#swift_library) function. If you're familiar with SwiftPM syntax, this is quite similar, except it's expressed in a style more akin to Python.

Afterwards, we call the [ios_application](https://github.com/bazelbuild/rules_apple/blob/master/doc/rules-ios.md#ios_application) function to outline our application. Most of the parameters should be easy to understand. The `visibility` parameter is handy in larger projects divided into modules. But for simpler projects like ours, it's perfectly fine to keep it public.

The remaining part of the `BUILD` file is responsible for adding Xcode support to our project, so we're not stuck coding in Notepad.

The `MyModule` directory houses the definition of a module. It's always a smart move to divide your project into separate, functional units.

Lastly, `Info.plist` and `Resources` function just like they would in Xcode.

## Working with Xcode

Now that the project is set up, we're ready to start working on it. Normally, we would just double-click the `.xcodeproj` file, but there isn't one available. Like many other third-party build systems, Bazel doesn't depend on `.xcodeproj` files internally. However, it can generate a temporary project file for our use. To create one, run the following command:

```
$ bazel run :xcodeproj
```

In a few seconds, an `App.xcodeproj` file should appear in the project's directory. Double-click it to open Xcode.

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d51ecd9ankr3833wk2an.png)

This brings us back to our comfort zone. Working with a Bazel-based project in Xcode should feel familiar. The key thing to remember is that when you want to change the project's structure (like adding files or modules), you need to make these changes in the `BUILD` files first, then re-generate the project.

## Preparing for Distribution

Let's assume we are ready to send the app for testing or submission to the App Store. How do we get the app archive? Typically, we would use the Archive action in Xcode. With Bazel, we don't even need Xcode for that. Instead, head over to the terminal, and run the following command:

```
$ bazel build -c opt :App
```

Here, we're instructing Bazel to build the target `:App`, which is our application (as seen in `BUILD`), in its Release configuration (where `opt` stands for "optimized").

Once the build completes, you should see the following lines:

```
Target //:App up-to-date:
  bazel-bin/App.ipa
```

This indicates that the build was successful, and the .ipa file can be found at `bazel-bin/App.ipa`. This file can then be submitted to the App Store using the Application Loader, or shared with friends or colleagues for AdHoc installation.

## Conclusion

Bazel is a powerful build system that can greatly simplify the build and distribution process of iOS applications, especially as they become more complex. By integrating Bazel with familiar tools like Xcode, you can harness the best of both worlds - the flexibility and power of Bazel, and the convenience and familiarity of Xcode. While it might take some getting used to, the pay-off in terms of improved efficiency and scalability makes it well worth the effort.

Happy coding!