Xcode Build Settings in Depth

Getting to the rock bottom of Xcode build settings.

If you have ever done any Mac OS or iOS development, you eventually had to deal with Xcode build settings.

Build Settings

So what are those are what do we know about them?

For a standard iOS project there’s roughly 500 build settings grouped into around 50 categories. These build settings control virtually every single aspect of how you app is built and packaged. At the very least, build settings is what makes Debug build so different from Release build.

In this article I’ll mostly focus on build settings that control the behavior of Apple Clang and Swift compilers and the linker.

🗄 Background Check

Build settings are not as simple as they may seem.

For starters, there are project and target level build settings as well as default OS settings. The default OS build settings are inherited on a project level and project level build settings are inherited on a target level.

Then there are .xcconfig files, which can be used to represent build settings in plain text format. The xcconfigs can be set on target and project level and add two more levels to inheritance flow.

Inheritance

To make things even more complicated, you can include other xcconfigs using C-like #include statements.

And then there’s much more to build settings and xcconfigs.

To get familiar with all of the above, I’d highly recommend to start with The Unofficial Guide to xcconfig files. Check out the rest of this amazing blog for even more hands-on information about understanding and managing build settings.

⬇️ Level Down

Now let’s get one more level down. When it comes to compiling and linking the source code, Xcode build system manages 3 main tools under the hood:

  • Clang Cxx compiler for compiling C/C++ and Objective-C/C++ code
  • Swift compiler
  • Linker to link all object files together

Xcode Tools

Each of those tools has its own set of command line flags. Clang compiler and linker flags are documented here. swiftc --help command provides list of Swift command line flags. Surprisingly, I failed to find online documentation similar to Clang.

When a build setting is set in Xcode UI, Xcode then translates it to appropriate flags for underlying tools. For example, setting GCC_TREAT_WARNINGS_AS_ERRORS to YES will add -Werror flag for Clang Cxx compiler.

Cxx Treat Warnings as Errors

Similarly, setting SWIFT_TREAT_WARNINGS_AS_ERRORS to YES will add -warnings-as-errors flag to all invocations of Swift compiler.

Swift Treat Warnings as Errors

Finally, some build settings are translated into flags for all three tools, for example, enabling code coverage using CLANG_ENABLE_CODE_COVERAGE = YES build setting, will translate into the following*:

  • -fprofile-instr-generate and -fcoverage-mapping for Clang compiler
  • -profile-coverage-mapping and -profile-generate for Swift compiler
  • -fprofile-instr-generate for linker

Code Coverage Flags


* Technically, it takes more than just setting CLANG_ENABLE_CODE_COVERAGE to enable code coverage, more details to come.


So, how does Xcode know which flags to map build settings to? Where is this mapping information stored and can it be extracted?

⚙️ Xcode Specs

The answer to the question in previous section is Xcode Specs or xcspecs.

Xcspecs are ASCII plist files with .xcspec file extension stored deep inside Xcode.app bundle. Xcode uses these specs to render build settings UI and to translate build settings to command line flags.

There are xcspecs for Clang compiler (Clang LLVM 1.0.xcspec), Swift compiler (Swift.xcspec) and linker (Ld.xcspec) as well as a number of xcspecs for core build system and other tools. These xcspecs reference each other and work together as a system.

Each xcspec contains specification for one or more tools, for example, Swift xcspec contains specification for Swift compiler tool, while Clang LLVM xcspec contais specifications for Clang compiler, analyzer, couple of migrators and an AST builder tool.

A tool specification includes such details as name, description, identifier, executable path, supported file types and more.

🔘 Options

In context of this article we are most interested in Options array entry of the tool’s specification, which is where information about build settings is stored. For example, the value of SWIFT_EXEC is stored as an option and is used by Xcode to resolve ExecPath = "$(SWIFT_EXEC)"; statement:

All build settings (options) have Name and Type properties. Name defines the build setting name, e.g. SWIFT_OPTIMIZATION_LEVEL.

Build settings may also have a Category, Description and DisplayName properties. For example, display name for SWIFT_OPTIMIZATION_LEVEL is Optimization Level.

A few build settings are hidden and never appear in Xcode UI. Some of them have corresponding comment in the code like // Hidden., but not all.

Types

There are 6 different types of build settings:

  • String
  • StringList
  • Path
  • PathList
  • Enumeration
  • Boolean
ℹ️ String and Path

A build setting of String type has, well, a string value.

Note that string value doesn’t have to be quoted using double quotes. As a matter of fact, some build setting that have integer values are represented using String type with default value set to 0, but not to "0".

Path type is identical to String when used in xcspecs.

My guess is that Path type build settings are handled differently with regards to escaping whitespaces and other special characters. They also get special treatment when resolving wildcard characters like *

ℹ️ StringList and PathList

StringList and PathList are used to represent list of string and path values correspondingly.

If not provided, default value is an empty list.

You can tell a list type in Xcode UI because it allows multiple values input:

List

ℹ️ Enumeration

Values of Enumeration type have a fixed list of values (cases), defined using Values key, for example:

Another good example is build settings that have YES_AGGRESSIVE or YES_ERROR value on top of YES and NO. These build settings are declared as enumerations too:

Enumeration

ℹ️ Boolean

Finally, values of Boolean type have either YES or NO value.

Note that only YES and NO are correct values for Boolean type, but not "YES" or "NO".

Boolean

🗺 Command Line Flags Mapping

As we already know, a lot of build setting values map to different command line flags. The mapping information is defined in xcspecs using one of the following keys:

  • CommandLineArgs
  • CommandLineFlag
  • CommandLinePrefixFlag
  • AdditionalLinkerArgs
ℹ️ Command Line Arguments

CommandLineArgs key-value entry is used to map build setting value to a list of command line arguments.

Scalar Types

A good example is SWIFT_MODULE_NAME build setting of String type:

The $(value) is resolved to current build setting value, e.g. MyModule value is mapped to -module-name "MyModule" Swift compiler flag.

List Types

For list types each build setting value in the list is mapped to one or more command line flags, for example:

If the value of CLANG_ANALYZER_OTHER_CHECKERS is "checker1" "checker2", it will be mapped to the following:

Enumeration Type

For enumerations types, mapping is defined for each enumeration case, for example:

  • ansi value is mapped to -ansi compiler flag.
  • compiler-default maps to no flags.
  • All other enumeration values map to -std=$(value) compiler flag. Note the use of "<<otherwise>>", this is similar to default: enum switch in C-like languages.
Boolean Type

Boolean types are mapped just like enumerations with 2 YES and NO cases.

ℹ️ Command Line Flag

CommandLineFlag is used to prepend command line flag to the value of build setting.

For example, SDKROOT is defined like so:

So if the value of SDKROOT is iphoneos, the corresponding Clang compiler flag will be -isysroot iphoneos.

In a way CommandLineFlag is a shorthand for using CommandLineArgs. I.e. in SDKROOT example, CommandLineFlag = "-isysroot"; could be replaced with CommandLineArgs = ("-isysroot", "$(value)");.

Handling of list, enumeration and Boolean types is similar to handling CommandLineArgs too. For example, given the definition:

The value like SYSTEM_FRAMEWORK_SEARCH_PATHS = "A" "B" "C" will be mapped to:

Enumerations deserve a special mention, because build flag mapping can be defined next to the value. A good example is MACH_O_TYPE:

ℹ️ Command Line Prefix Flag

CommandLinePrefixFlag maps build setting value to itself prefixed with a build flag.

It works just like CommandLineFlag, the only difference is that there’s no space between the build flag and the build settings value.

A good examples are LIBRARY_SEARCH_PATHS and FRAMEWORK_SEARCH_PATHS build settings of list type, where each list entry is mapped to -L"$(value)" or -F"$(value)" correspondingly.

Another similar build setting often used by developers is OTHER_LDFLAGS.

Finally, whenever you see a flag like -fmessage-length=0 it’s most likely mapped using prefix flag rule.

ℹ️ Additional Linker Flags

Certain Swift or Clang compiler build settings map not only to compiler flags, but to linker flags as well. Those build settings have an additional AdditionalLinkerArgs key-value pair, for example:

In this example, the -fobjc-arc flag will be added both to Clang compiler and linker invocations.

The way mapping works for different build setting types is identical to handling of CommandLineArgs.

References

As I’ve mentioned earlier, build settings from different xcspecs can reference each other.

Some build settings have a DefaultValue property. It can reference other build settings, e.g. DefaultValue = "$(BITCODE_GENERATION_MODE)";. Default value of some build settings is defined by referencing other build setting:

Default Value Reference

The references can be nested as well, for example:

Here $(DEPLOYMENT_TARGET_SETTING_NAME) will be first resolved into a value like SOME_SETTING and then $(SOME_SETTING) is resolved once again to a final value. Similar to how build settings are resolved in xcconfigs.

Build settings reference can be used inside CommandLineArgs and all other mapping key-value entries.

Conditions

Conditions are defined using Condition key-value pair and are used to control when certain build setting is enabled or not.

For example, SWIFT_BITCODE_GENERATION_MODE build setting only makes sense when ENABLE_BITCODE is set to YES:

Conditions have a C-like syntax, although string values do not have to be quoted. Such boolean operators as !, &&, ||, == and != can be used:

Other Properties

There are other properties, such as

  • Architectures - defines which target architectures the build setting is applicable for.
  • AppearsAfter - controls the order in which 2 specific build settings appear in Xcode UI.
  • FileTypes - list of applicable file types.
  • ConditionFlavors - must have something to do with the way conditions are checked.
  • Some other flags I didn’t have time to fully investigate yet.

👩‍🏫 Example

Let’s have a look at one build setting example and see how we can apply all the knowledge from this article to figure out which flags it will map to.

The build setting is CLANG_ENABLE_CODE_COVERAGE and it enables one very important feature - code coverage.

CLANG_ENABLE_CODE_COVERAGE is defined in Clang LLVM 1.0.xcspec but strangely maps to no flags at all…

However, CLANG_ENABLE_CODE_COVERAGE is referenced by CLANG_COVERAGE_MAPPING.

So now we know the Clang compiler flags added to compiler invocation when code coverage is enabled.

Additionally, there’s another build setting that controls the linker flag:

It also comes with a very detailed comment.

Finally, CLANG_COVERAGE_MAPPING is also defined in Swift.xcspec:

Now all the pieces of the puzzle come together and it’s clear how Xcode does the mapping.

Code Coverage Flags

The only problem is that both CLANG_COVERAGE_MAPPING and CLANG_COVERAGE_MAPPING_LINKER_ARGS are hidden and don’t show up in Xcode UI. So how do those get set if they don’t reference CLANG_ENABLE_CODE_COVERAGE in their default value?

The code coverage can be enabled by editing the scheme in Xcode UI or by passing -enableCodeCoverage YES to xcodebuild invocation

Default Value Reference

Xcode will then set both CLANG_COVERAGE_MAPPING and CLANG_COVERAGE_MAPPING_LINKER_ARGS to YES under the hood.

👷‍ Application

OK then, so it’s more or less clear how build settings are resolved, but what’s the practical application?

Well, there’s the purely academic application where you get to know how things work, which occasionally will come handy when you have hard time figuring out some build settings mess.

While the official Xcode Build Settings reference page is a good resource to use, the Extended Xcode Build Settings reference like this one includes information about compiler and linker flags, build settings cross-reference and more.

There are other ways to use this knowledge.

Let’s say you want to try out alternative build system like Buck or Bazel. Those build systems are gaining popularity these days. Buck was created by Facebook while Bazel came from Google. Other companies such as Uber, Airbnb and Dropbox use Buck; while Lyft is using Bazel to build their mobile apps.

Let’s further assume that over the years you have created and maintained a number of amazing xcconfig files. Those xcconfigs have all the compiler and linker build settings fine-tuned for your use. While moving to tools like Buck, you can’t just bring xcconfigs over, instead you’d want to translate xcconfigs to compiler and linker flags and then use those with Buck.

Well, now you have all the knowledge to do so. You’d need to start with reading and resolving xcconfigs and then map resolved build settings to build flags. It’s not a straightforward task and may take a while to implement. Luckily for you, there’s a Fastlane plugin that does just that.

Like many unofficial tools, this plugin is reverse engineering the ways Xcode works, so use it at your own risk.

Published: May 12 2019

blog comments powered by Disqus