Using Bazel with your iOS Projects
Peter Iakovlev
Posted on June 23, 2023
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 (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".
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 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 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.
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!
Posted on June 23, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.