At Airbnb, we recognize that developer experience is key to good engineering. Our team specifically — Mobile Developer Infra — has a goal of optimizing our mobile apps’ build times.
In June, we got Buck to successfully build our iOS app. This was a huge milestone for us: until we started working on it, Buck did not support mixed-language iOS projects, and our iOS codebase consisted of a pretty even mix of Swift and Objective-C. With this change, we are seeing 50% faster CI builds and a 30% smaller app size.
Hitting this milestone was a complicated process. In this post, we’d like to share technical details about what kind of challenges we faced, and how are we using Buck in our iOS codebase. Hopefully this is useful to others interested in a similar undertaking.
How an iOS App is Built
When we looked into how exactly Xcode builds an iOS project, we found this awesome post, which explains the process in detail.
In short, the build process includes these steps:
- Passing bridging header files (maintained by the author) and Swift source files to the
swifttool, generating two kinds of files:
.o, machine code files for generating the final executable binary.
*-Swift.h, containing all classes and interfaces defined in Swift code. These can be imported explicitly in Objective-C files that want to use functionality defined in Swift code.
- Passing all Objective-C source files and
clangtool, it generating
.ofiles for each Objective-C file.
- Passing all
ldcommand, which will link all machine code files and generate the final executable.
Differences Between Buck and Xcode
Buck uses roughly the same process outlined above to build iOS projects. However, there is one crucial difference that makes it more challenging to support a mixed-language projects like ours.
Xcode builds each module independently, producing dynamically-linked frameworks. For a particular module
M, the executable binary and related resources/assets end up under
App.app/Framework/M.framework in the final app folder.
Buck treats these modules as static libraries, linking them all together and producing a single executable binary. This approach can effectively reduce the binary size since:
a) If multiple modules are using the same resource/asset, it doesn’t need to copy the same file to each
b) It can strip more unused symbols, since all libraries are linked together statically.
This works perfectly in Objective-C-only or Swift-only projects. Unfortunately, this optimization creates problems in mixed-language projects.
Flag -import-underlying-module Does Not Work
-import-underlying-module build flag causes implicit imports of Objective-C files into Swift, within the same module. This flag unfortunately doesn’t work with Buck.
When Xcode generates frameworks, it generates
.hmap header map files to indicate header locations. The
swift tool later uses these files to import Objective-C headers. However, since Buck doesn’t generate independent frameworks, it doesn’t generate these files. Thus, the
-import-underlying-module flag doesn’t work in the
This means that we have to explicitly pass in bridging headers to the
swift tool. Doing this, however, results in a few more problems.
Unable to Use Bridging Header
Consider this example:
A.h contains the line
#import “B.h”, but
B.h is put under
folderB/. This works perfectly with Xcode with the help of
.hmap. But in Buck, this doesn’t work since it is not able to locate
In this PR, we updated Buck to allow it to generate header maps for use by the
swift tool, allowing the tool to locate header files and import them.
Unable to Locate Bridging Header Inside *-Swift.h
swift tool generates
*-Swift.h files, it explicitly imports bridging headers for Objective-C definitions. This breaks Buck builds.
According to Apple’s code, when using the
-import-underlying-module flag, the generated
*-Swift.h files import project headers. For example:
When providing bridging headers explicitly however (as we need to do with Buck) the generated
*-Swift.h files end up importing bridging header files directly:
As you can see, the imported path is a relative one. When another file imports this
*-Swift.h file, it won't be able to locate the bridging header.
In this commit, we updated Buck to pass
-iquote buckRootPath as a compiler argument. That specifically tells the
swift tool to look for the bridging header files at
@import Does Not Work
There are two ways to import header files into Objective-C,
@import doesn’t work in Buck since Buck doesn’t generate
This actually requires us to replace
@import M with
#import <M/M.h> and/or
#import <M/M-Swift.h>. This is simple enough for our own source code. However, it's a bit trickier for generated code.
*-Swift.h files, for instance, always use
To address this, we used an admittedly hacky solution, introducing this script to perform the replacement on the fly. In this commit, we added to Buck’s
apple_library build rule a new
objc_header_transform_script parameter, which allowed us to invoke the replacement script on all the
This change knocked down our last big blocker for using Buck.
To help illustrate the work we’ve done, we created this sample project, feel free to clone it and test it out for yourself!
Building a Mixed-Language Library
As mentioned before, when building a mixed-language library, we need to pass the bridging header file into the
apple_library build rule.
We treat each pod as an individual library, using a different build rule for each one.
In most cases, the pod includes its source code, so we can simply use
apple_library to build it.
When a pod only provides a compiled binary (e.g.
libSample.a), we use the
prebuilt_cxx_library build rule.
There are still some challenges ahead of us:
buck projectto generate Xcode project files. Our planned workflow involves using Xcode during local development and Buck in CI.
- Upgrading Buck to build libraries as modules, so that
-import-underlying-moduleworks and our hack can be removed.
- Optimizing the Buck cache for iOS. We found some room for improvement in the Buck caching mechanism, and will continue invest in it.
- Performing further analysis on how much we gain from switching from Xcode to Buck.