Conditional Xcode Build Settings

In the previous post, I described a problematic warning introduced by the new linker in Xcode 15. In it, I shared that the warning can be effectively disabled by passing a suitable argument to the linker:

OTHER_LDFLAGS = -Wl,-no_warn_duplicate_libraries

This simple declaration will address the problem on Xcode 15, but on Xcode 14 and earlier it will cause a link error because the old linker doesn’t recognize the argument. What we want to do if the project will continue to be built by both older and newer versions of Xcode, is to effectively derive a different value for OTHER_LDFLAGS depending on the version of Xcode itself. (Maybe more correctly, depending on the version of the linker, but for this example we’ll focus on varying based on Xcode version).

There’s no straightforward way to avoid this problem, but Xcode build settings offer a sophisticated (albeit brain-bendingly obtuse) mechanism for varying the value of a build setting based on arbitrary conditions. Technically this technique could be used in Xcode’s build settings editor, but because of the complexity of variable definitions, it’s a lot easier (not to mention easier to manage with source control) if you declare such settings in an Xcode configuration file. The examples below will use the declarative format used by these files.

The key to applying this technique is understanding that build settings can themselves be defined in terms of other build settings. For example:

OTHER_LDFLAGS = $(SUPPRESS_WARNING_FLAGS)
SUPPRESS_WARNING_FLAGS = -Wl,-no_warn_duplicate_libraries

These configuration directives have the same effect as the single-line definition above, because Xcode expands the SUPPRESS_WARNING_FLAGS setting and uses the result when setting the value of OTHER_LDFLAGS. Even more powerfully, you can nest build settings so that the value of one build setting is used to construct the name of another:

SHOULD_SUPPRESS = YES
OTHER_LDFLAGS = \((SUPPRESS_WARNING_FLAGS_\)(SHOULD_SUPPRESS))
SUPPRESS_WARNING_FLAGS_YES = -Wl,-no_warn_duplicate_libraries

Here, the final value for OTHER_LD_FLAGS will only include the specified flags if the SHOULD_SUPPRESS build setting is YES, because expanding SHOULD_SUPPRESS yields the build setting SUPPRESS_WARNING_FLAGS_YES. If SHOULD_SUPPRESS is NO, or any other value, then the expansion will lead to an undefined build setting, and thus will substitute an empty value.

Side note: when editing Xcode configuration files, keep one editor pane open to the configuration file, and one pane open to the Xcode build settings interface, focused on a project or target that depends on the configuration file. As you edit and save the file, Xcode updates the derived build settings in real time so you can check your work. For example, if you were to change the value of SHOULD_SUPPRESS to NO, you would see the “Other Linker Flags” value change to empty in Xcode.

Obviously, hard-coding SHOULD_SUPPRESS to YES as we did above isn’t going to solve the problem, because these configuration settings will cause the incompatible linker parameter to be passed on Xcode 14 and earlier.

The simple ability to nest build settings leads to a huge variety of clever tricks that allow you to impose a kind of declarative logic to settings. For example, here’s a cool technique I learned from the WebKit project:

NOT_ = YES
NOT_NO = YES
NOT_YES = NO

NOT_ equals YES? What does it meeeeaaan? It only makes since when you consider what happens when you combine NOT_ with another build setting that is defined as a boolean value:

SHOULDNT_SUPPRESS = \((NOT_\)(SHOULD_SUPPRESS))

This expands to $(NOT_YES) which in turn expands to NO. You can make sense of these exotic uses of nested build settings by slowly walking through the expansion, from the inside out. Each time you expand the contents of a $() construct, you end up with text that combines with the adjacent text to yield either a new build setting name, or the final value for a setting.

Finally, let’s apply a similar trick to the question of whether we’re running Xcode 15 or later. For this I am also leaning on an example I found in the WebKit sources. By declaring boolean values for several Xcode version tests:

XCODE_BEFORE_15_1300 = YES
XCODE_BEFORE_15_1400 = YES
XCODE_BEFORE_15_1500 = NO

We lay the groundwork for expanding a build setting based on the XCODE_VERSION_MAJOR build setting, which is built in:

XCODE_BEFORE_15 = \((XCODE_BEFORE_15_\)(XCODE_VERSION_MAJOR))
XCODE_AT_LEAST_15 = \((NOT_\)(XCODE_BEFORE_15))

In this case, on my Mac running Xcode 15.1, XCODE_BEFORE_15 expands to XCODE_BEFORE_15_1500, which expands to NO. XCODE_AT_LEAST_15 uses the aforementioned NOT_ setting, expanding to NOT_NO, which expands to YES.

Easy, right?

Putting it all together, we can return to the original example with SHOULD_SUPPRESS, and replace it with the more dynamic XCODE_AT_LEAST_15:

OTHER_LDFLAGS = \((SUPPRESS_WARNING_FLAGS_\)(XCODE_AT_LEAST_15))
SUPPRESS_WARNING_FLAGS_YES = -Wl,-no_warn_duplicate_libraries

OTHER_LDFLAGS expands to SUPPRESS_WARNING_FLAGS_YES on my Mac running Xcode 15.1, but on any version of Xcode 14 or earlier, it will expand to SUPPRESS_WARNING_FLAGS_NO, which expands to an empty value. No harm done.

I hope you have enjoyed this somewhat elaborate journey through the powerful but difficult to grok world of nested build settings, and how they can be used to impose rudimentary logic to whichever settings require such finessing in your projects.