From Bazel to XcodeGen to Tuist
Crafting a seamless and efficient iOS build process is crucial for delivering a top-notch user experience. In 2022, I worked on a significant transition: migrating a large project from the robust but complex Bazel to the more developer-friendly XcodeGen. This journey, while challenging, ultimately led us to discover the power of Tuist.
More interested in a how to? See my guide on how to integrate EarlGrey 2.0 using Tuist.
The Bazel Era: Power and Complexity
Tech companies usually turn to Bazel for iOS, renowned for its correctness and speed. Bazel’s deterministic build graph and caching capabilities can significantly improved build times, both locally and remotely. However, its complexity, particularly when integrating with Xcode, poses a significant hurdle that requires careful maintenance and management. Historically, these are some of the issues teams find when working with Bazel:
- Expert Dependence: Not many engineers possess the deep knowledge required to troubleshoot Bazel-related issues. This is specially true on iOS project where the tech stack is very different.
- Xcode Integration Challenges: Seamless integration with Xcode can be elusive, leading to workarounds and compatibility issues with new Xcode features every time there’s an upgrade.
- Third party dependency integration: Even though there are several ways to add third-party dependencies to a project (SPM, Cocoapods, Carthage, etc), none of these have a straightforward integration with Bazel. Some solutions such as XCHammer exist to bridge the gap between Bazel and Xcode, and while effective, they add another layer of complexity.
As our team evolved, the learning curve for maintaining this intricate system grew steeper. The decision was made: we needed a more manageable solution.
XcodeGen: A Breath of Fresh Air (Initially)
XcodeGen, with its declarative, YAML-based configuration, promises a simpler approach. A transition to use XcodeGen can be smooth depending on the size of the project. XcodeGen also has clear benefits for version control, maintainability, and integration with Xcode since it generates a native Xcode project as its output. Despite its benefits, there’s some risks that come along XcodeGen:
- Flaky Builds and Modulemap Issues: Converting targets to static libraries or viceversa, and managing custom modulemaps result in unpredictable build failures.
- Unchecked Flexibility: XcodeGen’s permissive nature allows for unchecked configurations, leading to errors that are difficult to debug. It also offers no guidance on correctness which can result on abuse or misuse of framework types. For example, a project will be using dynamic vs. static linking for target dependencies based solely on what developers set them to be, resulting in frequent problems with app size, unknown or duplicate symbols, and launch time regressions.
- Lasagna Code in YAML: Complex templates and nested configurations makes the project increasingly difficult to maintain.
While XcodeGen offers flexibility, it lacks the structure and safeguards needed for larger projects. A simple project generator is not enough, we need a smart one.
Tuist: Structure and Efficiency
Enter Tuist, a Swift-based project generation tool that addresses our core concerns:
- Extensible Swift Language: Tuist’s Swift-based configuration provides finer control and extensibility.
- Built-in Linter and Dependency Graph Checks: Tuist’s built-in linter helps identify and resolve dependency issues, such as upstream dependencies and static linking conflicts.
- Remote Caching Potential: Tuist’s caching solution offers a promising path to improve build times, surpassing attempts at this with XcodeGen and Derived Data.
- Deterministic SPM Dependency Management: Tuist’s
tuist install
command ensures reliable and consistent Swift Package Manager (SPM) dependency fetching. - Responsive Community: The Tuist maintainers are very responsive, providing invaluable support and timely solutions.
The EarlGrey2 Challenge
One problem with Tuist is that it wants you to do things in a certain way, which is both a good and a bad thing. The good is that it does things the way it does to improve reproducibility of builds. The bad is that there’s restrictions, similar as with Bazel. Most dependencies out there now support Swift Package Manager, CocoaPods, Carthage, or plain Xcode projects. These are usually easy to integrate in a regular project. I tend to stay away from CocoaPods and in fact haven’t used it in years now (Why? that’s a topic for a different post). So it becomes a hassle when there’s no SPM, Carthage or an Xcode project associated to a dependency.
And so EarlGrey2 comes into view. EarlGrey 2 is a UI test framework developed by Google, and not the regular framework, it’s integration is very custom, and as far as Tuist is concerned, it breaks a lot of rules. And it only has CocoaPods and an xcodeproj
. The Xcode project works for project generators such as XcodeGen, but it doesn’t for Tuist because it breaks some of the internal logic that is needed for better caching.
It is not easy feat, but I have managed to successfully integrate EarlGrey2 in multiple projects now. Thanks in great part to the great support and collaborating closely with the Tuist team. The Tuist team’s responsiveness and willingness to incorporate changes from collaborators is in my opinion, one of Tuist’s greatest assets.
All I want to add in this section, is that integrating EarlGrey2 into an iOS project is an odd ball, and even though there were hurdles, it is still possible to work with Tuist to make it happen. There’s another post describing the detailed integration. More interested in a how to? See my guide on how to integrate EarlGrey 2.0 using Tuist.
Benefits of Tuist
In the work I’ve done, there have been some clear benefits of using Tuist vs. other options:
- Improved clean build times. In all projects and benchmarks I ran, I’ve seen close to 20% improved build times using Tuist defaults.
- Cleaner way to establish project conventions. Everything can be done programatically, and nicely modular hiding unnecessary logic from project manifest writing.
- Caching works, to an extent. It is not Bazel but it’s a good compromise and place in between.
- Caching works even better if good practices are put in place such as proper dependency injection and appropriate separation of interface and implementation in project modules. This is, however, true however you maintain your iOS project (and a good topic for another post).
No comments yet. Be the first to comment!