Raja Software Labs logo

A Peek Into the Xcode Build System

Xcode abstracts out most details of the build system from developers. On a high level, creating and running an iOS app is just a matter of reusing an Xcode template, adding some files with code to the project, selecting a target and clicking the “Play” button, and developers can get away with little knowledge of the underlying build steps performed. However, as a project grows in size and complexity, developers face several challenges where understanding the build system becomes crucial.

Before diving into the build system, let’s touch upon some of these challenges / problems:

  1. Identifying the cause of a build failure: Swift / ObjC compiler errors are usually easy to spot and fix. However, in cases where the build failure reason related to actool / ibtool command failing, without knowledge of what these tools do, it’s hard to pinpoint a location where we can start debugging for problems in the project.
  2. Debugging Linker errors: If your source files compile fine but the build fails later with something like:
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    
    This means the linking is failing and understanding what the linker does and when it runs is important to track down such issues.
  3. Improving build times: As the project gets larger, the build time increases. However, to make sure we still get the best possible build time, it is crucial to understand that the build system creates a dependency graph and then begins the compilation process. Modularizing your code and setting up the right dependency order are key to improving build times.
  4. Setting up your project to use a dependency manager (Cocoapods / Carthage / Swift Package Manager): If you choose to use a dependency manager for your project to use external dependencies / manage internal dependencies, understanding how the asset catalogs, interface builder files and entitlements defined in these dependencies are going to finally get collated and bundled into the final application is key to make sure the application builds and runs fine.
  5. Switching to a new build system (e.g. Bazel): Bazel is a completely different build system. While it has its own ways to create a dependency graph / maintain build cash, it internally uses the same compiler (clang / swiftc) and development tools (ibtool / actool) which come bundled in Xcode developer tools to compile / process Apple specific file formats.

Now that we see the value of understanding the build system, let’s create a simple app using a predefined Xcode template, look at the detailed build logs and peek into the exact steps which are performed + highlight some tools which Xcode uses internally for these tasks.

Before we begin

  1. The steps written below and build logs mentioned are generated using Xcode 14.3, iOS 16.4 on Mac OS Ventura 13.4. If you have different versions, these might differ a bit but the general essence of the analysis below should remain the same.
  2. The file paths in the build log snippets are replaced with /---/ for privacy and brevity.

Steps followed to create the Xcode project

  1. Open Xcode.
  2. Go to Files > New Project.
  3. Under the iOS tab, select App and then Next.
  4. Enter the following project information and then click "Next".
    1. Project Name (e.g. “Test”)
    2. An organization identifier (e.g. "com.rsl").
    3. In "Interface" select "Storyboard".
    4. Un-select "Use Core Data" and "Include Tests".
  5. Select any directory where you would like to save your project.
  6. Click “Create”.

Accessing the build logs

  1. Go to the Report Navigator.
  2. Select the build operation you just performed.
  3. Select “All” and “All Messages” in the right hand panel.
  4. Then click on Export to export all build logs to a file.
  5. Finally, open this file and analyze.
  6. Important note: We are looking at the log for a clean build.

Perusing the build logs and key takeaways

1. Dependency Graph

Right at the start of the build logs, we notice this:

Showing All Messages

Prepare build
note: Building targets in dependency order

Building targets in dependency order

Computing target dependency graph and provisioning inputs

Takeaway: Xcode first parses the xcodeproj file and gathers information about everything it needs in order to build the final app binary. This includes all build intermediates like static / dynamic libraries, swift packages, source code files etc. It then builds out a directed graph data structure starting at the root node which is your app and, step wise, keeps adding new nodes as it uncovers more dependencies. Each node points to all its own build dependencies. This means that the leaf nodes of the final graph will be intermediates which can be built independently without any dependencies. While the construction of the graph starts at the root node, the actual build process starts at every leaf node and Xcode traverses up the dependency graph to finally build your app.

2. Derived Data and Build cache

The next thing in the build logs is this:

Create build description
Build description signature: c761a01203a96d49bcee9cdefae4cdca

Build description path: /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/XCBuildData/c761a01203a96d49bcee9cdefae4cdca.xcbuilddata

Takeaway: The build system uses the “Derived Data” folder to store any build cache / artifacts. Inside Derived Data, it creates a folder unique to your project (Test-fcfjnqsymzbzvmfaludqtnggsxoq), then generates a new hash for each unique build (c761a01203a96d49bcee9cdefae4cdca) and stores any build specific artifacts there using this hash in a new folder (c761a01203a96d49bcee9cdefae4cdca.xcbuilddata). This enables Xcode to keep a history of each build / run / test action it has performed and you can see all these in the “Reports navigator”. Further, the build cache also provides the build system access to older build artifacts which in turn help it to perform incremental builds where applicable.

3. Target and build configuration

Next, we notice this:

Build target Test of project Test with configuration Debug

Takeaway: Depending on your selected target and scheme, Xcode determines which exact target to build and also determines the build configuration to use (which is defined in your selected scheme). In the above log, it has determined that the Test target needs to be built using the Debug configuration.

4. Entitlements

Next, the logs mention this:

WriteAuxiliaryFile /---/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-
iphonesimulator/Test.build/DerivedSources/Entitlements.plist (in target 'Test' from project 'Test')

cd /---/Test

write-file /---/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-
iphonesimulator/Test.build/DerivedSources/Entitlements.plist

WriteAuxiliaryFile /---/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-
iphonesimulator/Test.build/DerivedSources/Entitlements-Simulated.plist (in target 'Test' from project 'Test')

cd /---/Test

write-file /---/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-
iphonesimulator/Test.build/DerivedSources/Entitlements-Simulated.plist

Takeaway: Xcode then processes any entitlement files for your app and copies them over to the build intermediates folder so these can be included in the final package it builds. Entitlement files include information about app permissions and capabilities etc. When code signing, these entitlements are embedded into the app binary and the app code can only access services from iOS for which it has entitlements defined for.

5. Storyboard / xib compilation

CompileStoryboard /--/Test/Test/Base.lproj/LaunchScreen.storyboard (in target 'Test' from project 'Test')
cd /--/Test

/--/Xcode.app/Contents/Developer/usr/bin/ibtool --errors --warnings --notices --module Test --output-partial-info-plist /--/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Base.lproj/LaunchScreen-SBPartialInfo.plist --auto-activate-custom-fonts --target-device

iphone --target-device ipad --minimum-deployment-target 16.4 --output-format human-readable-text --compilation-directory /--/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Base.lproj

/--/Test/Test/Base.lproj/LaunchScreen.storyboard
LinkStoryboards (in target 'Test' from project 'Test')
cd /--/Test

/--/Xcode.app/Contents/Developer/usr/bin/ibtool --errors --warnings --notices --module Test --target-device iphone --target-device ipad --minimum-deployment-target 16.4 --output-format human-readable-text --link

/--/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-
iphonesimulator/Test.app /--/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-
iphonesimulator/Test.build/Base.lproj/LaunchScreen.storyboardc /--/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-
iphonesimulator/Test.build/Base.lproj/Main.storyboardc

Takeaway: Yes, Storyboards and Xib files are compiled and linked, though they are simple xml files. The reason is that the Interface builder in Xcode provides ways (like IBOutlet, IBAction etc.) to be able to link / reference elements defined in these files with source code and hence Xcode processes them. Further, the tool used to compile / link them is the ibtool which is bundled into the Xcode’s developer tools.

6. Asset catalog compilation

CompileAssetCatalog /---/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app

/---/Test/Test/Assets.xcassets (in target 'Test' from project 'Test')
cd /---/Test
/---/Xcode.app/Contents/Developer/usr/bin/actool --output-format human-readable-text --notices --warnings --export-dependency-info /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/assetcatalog_dependencies --output-partial-info-plist

/---/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/assetcatalog_generated_info.plist --app-icon AppIcon --accent-color AccentColor --compress-pngs --enable-on-demand-resources YES --filter-for-thinning-device-configuration iPhone14,7 --filter-for-device-os-version 16.4 --development-region en --target-device iphone --target-device ipad --minimum-deployment-target 16.4 --platform iphonesimulator --compile /---/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app /---/Test/Test/Assets.xcassets

Takeaway: The actool comes bundled with Xcode developer tools and is used to compile the asset catalogs added to your app. Asset catalogs are sets of images with different resolutions and sizes and, at runtime, iOS uses these to select the image which is most appropriate for display on the device screen it is running on.

7. Source code compilation

The source code compilation starts after this and we see these build logs (truncated for brevity):

SwiftDriver Test normal x86_64 com.apple.xcode.tools.swift.compiler (in target 'Test' from project 'Test')
cd /---/Test builtin-SwiftDriver -- /---/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc -module-name Test -Onone -enforce-exclusivity\=checked @/---/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Objects-
normal/x86_64/Test.SwiftFileList -DDEBUG -sdk

/---/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator16.4.sdk -target x86_64-apple-ios16.4-simulator -enable-bare-slash-regex -g -module-cache-path
...
...

/---/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Objects-normal/x86_64/Test-Swift.h -working-directory /---/Test -experimental-emit-module-separately -disable-cmo

SwiftEmitModule normal x86_64 Emitting\ module\ for\ Test (in target 'Test' from project 'Test')
cd /---/Test builtin-swiftTaskExecution -- /---/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend -frontend -emit-
module -experimental-skip-non-inlinable-function-bodies-without-types /---/Test/ViewController.swift /---/Test/AppDelegate.swift
...
...
SwiftCompile normal x86_64 Compiling\ ViewController.swift
/---/Test/ViewController.swift (in target 'Test' from project 'Test')
cd /---/Test
builtin-swiftTaskExecution --
/---/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-frontend -frontend -c -
primary-file /---/Test/ViewController.swift
/---/Test/AppDelegate.swift
...
...

SwiftMergeGeneratedHeaders /---/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-
iphonesimulator/Test.build/DerivedSources/Test-Swift.h /---/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Objects-
normal/x86_64/Test-Swift.h (in target 'Test' from project 'Test')

cd /---/Test

builtin-swiftHeaderTool -arch x86_64 /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Objects-normal/x86_64/Test-Swift.h -o /---/Xcode/DerivedData/Test-
fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-
iphonesimulator/Test.build/DerivedSources/Test-Swift.h

Takeaways: The Swift compilation happens in roughly the following steps:

  1. The swift-driver is invoked first and is responsible for “driving” / coordinating the overall compilation of the Swift source code.
  2. It invokes swiftc to first create a Swift module for your source code. All Swift source code in our project is part of one single module (named as Test by default).
  3. For compiling the source code, the swift-driver then uses swift-frontend which is a proxy and it either invokes the swift interpreter or the compiler based on the task at hand.
  4. As the final step in the compilation process, a bridging header is <module>-Swift.h generated and packaged.

8. Linking

The next step after the compilation is to link all intermediates together:

Ld 
/---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app/Test normal (in target 'Test' from project 'Test')
    cd /---/Test
    /---/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -Xlinker -reproducible -target x86_64-apple-ios16.4-simulator -isysroot /---/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator16.4.sdk -L/---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/EagerLinkingTBDs/Debug-iphonesimulator -L/---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator -F/---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/EagerLinkingTBDs/Debug-iphonesimulator -F/---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator -filelist /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Objects-normal/x86_64/Test.LinkFileList -Xlinker -rpath -Xlinker @executable_path/Frameworks -dead_strip -Xlinker -object_path_lto -Xlinker /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Objects-normal/x86_64/Test_lto.o -Xlinker -export_dynamic -Xlinker -no_deduplicate -Xlinker -objc_abi_version -Xlinker 2 -fobjc-link-runtime -L/---/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/iphonesimulator -L/usr/lib/swift -Xlinker -add_ast_path -Xlinker /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Objects-normal/x86_64/Test.swiftmodule -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __entitlements -Xlinker /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Test.app-Simulated.xcent -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __ents_der -Xlinker /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Test.app-Simulated.xcent.der -Xlinker -no_adhoc_codesign -Xlinker -dependency_info -Xlinker /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Objects-normal/x86_64/Test_dependency_info.dat -o /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app/Test

Takeaway: All the required libraries including those compiled as part of your project along with any Apple libraries or external dependencies are linked together. ld is the linker used and it is invoked using the clang binary / command (since it is part of the clang tool chain). For the sake of completeness, clang is the default compiler toolchain for building ObjC on Apple platforms (if you are wondering why gcc works on your machine, run gcc --version on the terminal and look at the logs to realize that it just forwards the call to clang).

9. Code signing and .app creation

Right at the end of the build logs, we see this:

CodeSign /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app (in target 'Test' from project 'Test')
    cd /---/Test
    
    Signing Identity:     "-"
    
    /usr/bin/codesign --force --sign - --entitlements /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Intermediates.noindex/Test.build/Debug-iphonesimulator/Test.build/Test.app.xcent --timestamp\=none --generate-entitlement-der /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app

RegisterExecutionPolicyException /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app (in target 'Test' from project 'Test')
    cd /---/Test
    builtin-RegisterExecutionPolicyException /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app

Validate /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app (in target 'Test' from project 'Test')
    cd /---/Test
    builtin-validationUtility /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app

Touch /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app (in target 'Test' from project 'Test')
    cd /---/Test
    /usr/bin/touch -c /---/Xcode/DerivedData/Test-fcfjnqsymzbzvmfaludqtnggsxoq/Build/Products/Debug-iphonesimulator/Test.app

Takeaway: The final stage is signing and bundling the entitlements into the final .app. codesign is the tool used for this and it comes included in Xcode developer tools.

Note: At this point, we have a .app file which can be deployed to an iOS simulator. However, to be able to run this on a device we need to Archive > Distribute and code sign the package to generate the final .ipa file (which is out of the scope of this post.)

Conclusion

This is how an iOS application is built when we click the run / build button on Xcode. Let’s quickly summarize what we learnt and separately summarize the tools we discovered:

  1. Xcode first builds a dependency graph of everything required to build the targets in your project.
  2. It then determines the target you selected and the build configuration to be used.
  3. It then builds entitlements, interface builder files and any asset catalogs the project needs.
  4. Then it proceeds to compile the source code (Swift / ObjC).
  5. Next, it links all the compiled code together along with any external dependencies required.
  6. Finally, it bundles everything into a .app bundle which can then be archived and code signed to generate a distributable .ipa file.

Tools used:

  1. Interface Builder tool (ibtool): Used to parse and process any interface builder files (storyboard / xib) in your project.
  2. Asset Catalog tool (actool): Used to parse, process and bundle asset catalogs present in your project.
  3. Swift compiler: swift-driver, swiftc, swift-frontend work together to compile any swift files in the code.
  4. clang: Comes as part of the LLVM toolchain bundled in Xcode developer tools. Its used to compile any ObjC source code and link everything together.
  5. codesign: Used to process entitlements and bundle them into the .app bundle. This is also used for the final code signing process when you want a .ipa which can be distributed.

That’s all for this post, folks. Hope you learnt something and found this information helpful.

Happy Coding!