Upgrade to Pro — share decks privately, control downloads, hide ads and more …

DevFest2025 - Turbocharge Your Xcode Build with...

DevFest2025 - Turbocharge Your Xcode Build with Bazel

Avatar for Kyaw Zay Ya Lin Tun

Kyaw Zay Ya Lin Tun

November 21, 2025
Tweet

More Decks by Kyaw Zay Ya Lin Tun

Other Decks in Programming

Transcript

  1. Today’s Agenda 1. Introduction 2. What is a Build System?

    3. How Xcode’s Build System Works 4. Why Xcode Builds Become Slow 5. What is Bazel 6. How Bazel Build Work 7. Why Bazel? 8. Drawbacks 9. When to use Bazel? 10.Conclusion
  2. I am Kyaw the Monkey • Senior iOS Engineer @ZALORA

    Group • Indie hacker of a few bets - PhayarSar, Griddy, Rohingya Keyboard • Interested in DevX, platform engineering and compilers • @KyawTheMonkey on X, GitHub, Medium etc Hello DevFest 👋
  3. PhayarSar, the Buddhist Prayers App, will undergo a full rebrand

    in 2026. Stay tuned for what’s coming next!
  4. iOS projects have become larger, more modular, more distributed, and

    more dependent on CI, but Xcode’s build system hasn’t kept pace :( The ecosystem has evolved, and project scale has grown in multiple dimensions
  5. What it means to be a large-scale iOS codebase? •

    Swift • Objective-C • Even C++ ??? • And of course, Objective-C++ • Swift Package Manager • CocoaPods • Carthage • And many more…
  6. • Feature fl ags, routing layers, dependency injection, networking stacks

    • Multiple frameworks / shared code layers • Macro / Build tool plugin / Package plugin • Swift has slower compile times compared to Obj-C (well, more or less) • Lacks a fully stable incremental compilation model • Distributed teams need consistency • Derived data issues are common • iOS, Android, Backend, Web, Infra in one repository for shared code and tooling
  7. swiftc • Parsing • Semantics Analysis • Clang importer •

    SIL Gen • SIL Guaranteed Transformations • SIL Optimisation • LLVM IR Gen
  8. This shows how incremental build only rebuilds a ff ected

    fi les. Build system can execute in parallel any fi les whose inputs don’t depend on each other. Xcode tries this, but complex implicit dependencies can reduce parallelism.
  9. • Xcode automatically scans dependencies between fi les and targets

    • Large Swift modules can drastically slow both cold and incremental builds. • A change in one fi le can trigger rebuilds of many dependent fi les due to cross-module type inference • Implicit or hidden dependencies (like headers or bridging fi les) often force unnecessary recompilation.
  10. • Xcode relies heavily on state (intermediate build products) in

    DerivedData • Compiled object fi les, caches and indexing info • Speci fi c to machine and Xcode version 🚨 • “Works on my machine” becomes a daily problem • Corruption in DerivedData may cause unnecessary rebuilds • rm -rf Library/Developer/Xcode/DerivedData is often needed to fi x weird build issues
  11. Apple has been developing a new build-level caching solution, similar

    to those found in systems like Bazel and Buck. This new caching capability has been available since Xcode 26. The system attempts to ensure reproducible artifacts (a core idea of Bazel-style hermetic builds)
  12. Background info of CAS • Swift compiler in Xcode 15+

    uses CAS to cache compiled artifacts by content hash • A build system that uses CAS computes a unique fi ngerprint for each build task • This fi ngerprint is based on all the inputs and outputs associated with that build task • If the build system can safely assume it knows every input and output involved, it can generate a reliable hash • Using that hash, the system can check whether an identical build task has been executed before • If a previous result exists in the CAS, the build system can reuse the cached output instead of running the task again
  13. However, even with CAS • Xcode builds still depend on

    • Derived data • Hidden build settings • Only applies to Swift compilation, not the full build (linking, assets, bridging headers etc) • CAS relies on knowing all inputs, but Xcode may miss some, resulting in unnecessary rebuilds
  14. • Modularise the codebase, preview apps, pre-compiled binary frameworks •

    Use custom build system, ld64, ld_prime (for Xcode 15+) along those lines • Buy MacStudio or set up a device farm (using Ansible) for distributed build • Migrate to Tuist modules (which uses Xcode build) and leverage binary caching • XCCacheAgent by Thuyen Trinh
  15. Eager linking for build parallelisation Normally, if Target A depends

    on Target B, the build system must wait for Target B to fi nish linking before it can start linking Target A. With eager linking turned on, Xcode creates a TBD fi le for Swift-only frameworks or dylib. This placeholder tells the linker “Target B will exist,” so Target A can start linking immediately, without waiting for Target B to fi nish.
  16. Build active architecture only for debug Xcode only compiles the

    architecture that your current simulator uses. This means fewer only one architecture to compile, fewer object fi les, and faster incremental builds.
  17. Debug build should us DWARF This avoids an extra symbol

    extraction step during compilation and linking
  18. Installation • Two options to run Bazel • Via bazel

    directly or • Via bazelisk • I choose bazelisk as it • supports multiple bazel versions • can manage project’s bazel version via .bazelversion fi le $ brew install bazelisk
  19. Step 1. Create a new directory $ mkdir SimpleBazelApp $

    open -a 'Visual Studio Code’ SimpleBazelApp In simple terms, you create a new folder called “SimpleBazelApp” and open that folder inside VS Code
  20. Step 2. Create MODULE.bazel fi le The directory containing MODULE.bazel

    is e ff ectively treated as the project root for dependency resolution MODULE fi le for Bazel can be think of as Package.swift for SwiftPM; it de fi nes your module, its dependencies, and how external packages are resolved
  21. What are Bazel rulesets? • Collections of Bazel rules and

    helper scripts that de fi ne how to build, test, and package code for a speci fi c language or platformLack of knowledge of Xcode build system; Bazel also has a steep learning curve • Each rule in a ruleset speci fi es inputs, outputs, and actions that Bazel can execute • Their main purpose is to provide repeatable, declarative build instructions • You can write your own Bazel rules (topic for an another talk)
  22. module( name = "simple-bazel-app", version = "1.0.0") bazel_dep( name =

    "apple_support", version = "1.22.1") bazel_dep( name = "rules_apple", version = "4.0.1") bazel_dep( name = "rules_swift", version = "3.0.2") The name of the Bazel module. Other Bazel modules can depend on your module using this name. It is conceptually similar to the package name in SwiftPM’s Package.swift, npm’s “name” fi eld, or Cargo’s package name.
  23. module( name = "simple-bazel-app", version = "1.0.0") bazel_dep( name =

    "apple_support", version = "1.22.1") bazel_dep( name = "rules_apple", version = "4.0.1") bazel_dep( name = "rules_swift", version = "3.0.2") Responsible for registering Xcode and the platform SDKs as a Bazel toolchain.
  24. module( name = "simple-bazel-app", version = "1.0.0") bazel_dep( name =

    "apple_support", version = "1.22.1") bazel_dep( name = "rules_apple", version = "4.0.1") bazel_dep( name = "rules_swift", version = "3.0.2") Provides rules for packaging and running code on Apple platforms.
  25. module( name = "simple-bazel-app", version = "1.0.0") bazel_dep( name =

    "apple_support", version = "1.22.1") bazel_dep( name = "rules_apple", version = "4.0.1") bazel_dep( name = "rules_swift", version = "3.0.2") Provides build rules and utilities for compiling and testing Swift code.
  26. • Created a new directory called Sources • SimpleBazelApp.swift •

    CounterView.swift • Created a new directory called Resources • Info.plist
  27. Step 5. Create a BUILD fi le • Bazel works

    with packages, and a package is a collection of targets • The directory containing the BUILD fi le is the package • A BUILD fi le de fi nes targets • Smallest unit Bazel can build, test or package • Targets can be iOS app, libraries, binaries, tests or other artefacts • Every directory with code usually has a BUILD fi le to declare its targets (an hence, it becomes a package)
  28. Once you create a BUILD fi le, the directory becomes

    a Bazel package. A Bazel module is a collection of packages. So, a module can contain many packages (directories with BUILD fi les), but, there can be only one MODULE.bazel fi le in the module to declare its dependencies.
  29. • So far, I have introduced three Bazel concepts •

    MODULE.bazel fi le • Will de fi ne the Bazel module • Load external dependencies (rulesets) which will then be used by BUILD fi les • BUILD fi le (Package) • Denotes the containing directory as a Package • Target • With help of rules from rulesets (de fi ned in the Module fi le), Build fi le de fi nes one or more targets
  30. My Module Package A Target 1 (iOS app) Rulesets and

    dependencies Target 2 (macOS app) Target 3 (CLI) Package B Target 1 (iOS app) Target 2 (macOS app) Target 3 (CLI)
  31. Bazel targets are de fi ned by instance of rules

    load("@rules_apple//apple:ios.bzl", "ios_application") load("@rules_swift//swift:swift.bzl", “swift_library") swift_library( name = "lib", srcs = glob(["Sources/*.swift"]), ) ios_application( name = "SimpleBazelApp", bundle_id = "com.kyaw.SimpleBazelApp", families = ["iphone", "ipad"], infoplists = ["Resources/Info.plist"], minimum_os_version = "26.0", deps = [":lib"] )
  32. Load necessary rules to de fi ne targets load("@rules_apple//apple:ios.bzl", "ios_application")

    load("@rules_swift//swift:swift.bzl", “swift_library") swift_library( name = "lib", srcs = glob(["Sources/*.swift"]), ) ios_application( name = "SimpleBazelApp", bundle_id = "com.kyaw.SimpleBazelApp", families = ["iphone", "ipad"], infoplists = ["Resources/Info.plist"], minimum_os_version = "26.0", deps = [":lib"] )
  33. With “swift_library” rule, de fi ne a target named “lib”

    that compiles all Swift source fi les in the Sources directory load("@rules_apple//apple:ios.bzl", "ios_application") load("@rules_swift//swift:swift.bzl", “swift_library") swift_library( name = "lib", srcs = glob(["Sources/*.swift"]), ) ios_application( name = "SimpleBazelApp", bundle_id = "com.kyaw.SimpleBazelApp", families = ["iphone", "ipad"], infoplists = ["Resources/Info.plist"], minimum_os_version = "26.0", deps = [":lib"] )
  34. “SimpleBazelApp” is an another target, that uses “lib” target as

    a dependency load("@rules_apple//apple:ios.bzl", "ios_application") load("@rules_swift//swift:swift.bzl", “swift_library") swift_library( name = "lib", srcs = glob(["Sources/*.swift"]), ) ios_application( name = "SimpleBazelApp", bundle_id = "com.kyaw.SimpleBazelApp", families = ["iphone", "ipad"], infoplists = ["Resources/Info.plist"], minimum_os_version = "26.0", deps = [":lib"] )
  35. Step 7. Build your iOS application $ bazel build SimpleBazelApp

    SimpleBazelApp refers to the target you de fi ned earlier via ios_application rule.
  36. But wait, Bazel requires a speci fi c kind of

    rule-based directory layout where each package has its own BUILD fi le, unlike Xcode. So, how are we going to migrate our existing Xcode projects??? Two Xcode projects, one for Xcode Build and the other for Bazel?
  37. Well… it depends. Ideally, you don’t need two separate Xcode

    projects. Xcode acts as your editor while Bazel handles the actual build, both sharing the same source fi les.
  38. However, achieving this setup depends heavily on how your project

    is structured, and many teams build internal CLI tools to help migrate code, generate Bazel targets, or manage modules. It’s also possible to streamline this using XcodeGen. You can de fi ne a module template in a YAML fi le and pass arguments such as module name or source paths through a Swift Argument Parser based command-line tool to generate consistent Xcode project fi les.
  39. • A remote caching system requires the build system to

    provide fully reproducible builds where a build target’s inputs can be precisely identi fi ed and will reliably generate the same output on any machine • Low-level dependencies that don’t change very often can be built once and shared across other developers rather than having to be rebuilt by each • Compared to Buck, Bazel bene fi ts from a more active community, especially for iOS. Major adopters including Google, LinkedIn, and several early users have achieved impressive and satisfying outcomes • Can be con fi gured and extended to work with languages that are not o ff i cially supported by the tool itself • Can have more than just building, eg, dynamic code generation
  40. • Your team members need to learn Starlank (dialect of

    Python), a new language, to maintain the project’s build con fi guration • Lack of knowledge of Xcode build system, and tooling • Must be able to analyse the codebase, and its dependencies • Master static and dynamic linking behaviours • Able to use tools like XcodeGen to sca ff old modules • If required, need to develop/maintain internal tools (CLIs) to speed up feature development • Not every company can a ff ord to invest in mobile platform/infra squad • Bazel also has a steep learning curve
  41. • Because Bazel is a third-party build system, it doesn’t

    automatically support new Xcode features and must wait for community updates to stay compatible. • Eg, M1 Mac was introduced Nov 2020 but Bazel only fully supported it January 2022 • That means, you may contribute or rely on the Bazel community to keep the tooling and resources up to date • AFAIMC, Bazel has a limited resources, poor migration guide, and most of the tutorials are outdated
  42. • When you have the capacity to maintain the build

    tool • Mobile platform team • You have at least 10+ iOS developers working on your codebase • You have a massive codebase where the duration of the build time on each and every dev’s machine surpasses the time you have to invest in maintaining Bazel modules • Or you wanna work at a speci fi c company that uses Bazel
  43. • If it is for personal development, • Go for

    it, period • If it is for your team or for you company • Let the data speak for you • Make sure you have a migration strategy • Monitoring and managing build time should be a shared-responsibility • Haven’t covered the remote build caching, but, Bazel isn’t your only option
  44. References • https://bazel.build/migrate/xcode#create-workspace • https://engineering.mercari.com/en/blog/entry/20221215-16cdd59909/ • https://dev.to/petertech/using-bazel-with-your-ios-projects-g7e • https://brentley.dev/how-to-migrate-an-ios-app-to-bazel/ •

    https://medium.com/gojekengineering/gojeks-journey-to-3x-faster-ios-builds-with- bazel-90fbe3f22f81 • https://medium.com/better-programming/building-ios-apps-faster-using-bazel- b960f6788fab • https://www.avanderlee.com/optimization/analysing-build-performance-xcode/ #build-settings-to-speed-up-build-performance
  45. • https://github.com/CyrilCermak/modular_architecture_on_ios • https://pewpewthespells.com/blog/static_and_dynamic_libraries.html • https://varun04tomar.medium.com/xcode-build-system-know-it- better-96936e7f52a • https://medium.com/@cyrilcermak/exploring-ios-es-mach-o-executable- structure-aa5d8d1c7103

    • https://www.objc.io/issues/6-build-tools/mach-o-executables/ • https://medium.com/@shivayadav2820/unlocking-ios-a-comprehensive- guide-to-penetration-testing-on-apple-devices-2-5df8f4d72930 • https://medium.com/@donblas/fun-with-rpath-otool-and-install-name-tool- e3e41ae86172
  46. • https://github.com/aidansteele/osx-abi-macho- fi le-format-reference/blob/ master/Mach-O_File_Format.pdf • https://anuragajwani.medium.com/visualising-the-build-process-of-your-ios- app-with-xclogparser-7a3891dfd96f • https://blog.e

    fi ens.com/post/luibo/osx/macho/ • https://bpoplauschi.github.io/2021/10/24/Intro-to-static-and-dynamic- libraries-frameworks.html • https://github.com/swiftlang/swift/blob/main/docs/DriverInternals.md • https://eisel.me/lld • https://www.emergetools.com/blog/posts/static-vs-dynamic-frameworks-ios- discussion-chat-gpt