Monthly Archives: October 2023

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.

Xcode 15 Duplicate Library Linker Warnings

Apple released Xcode 15 a couple weeks ago, after debuting the beta at WWDC in June, and shipping several beta updates over the summer.

I’ve been using the betas on my main work machine, and for months I’ve been mildly annoyed by warnings such as these:

ld: warning: ignoring duplicate libraries: '-lc++'
ld: warning: ignoring duplicate libraries: '-lz'
ld: warning: ignoring duplicate libraries: '-lxml2'
ld: warning: ignoring duplicate libraries: '-lsqlite3'

You may have run into similar warnings with other libraries, but warnings about these libraries in particular seem to be very widespread among developers. Even though I’ve been seeing them all summer, and have been annoyed by them, I made the same mistake I often make: assuming that the problem was too obvious not to be fixed before Xcode 15 went public. Alas.

So what happened in Xcode 15 to make this suddenly appear, and why haven’t they gone away? The root of the problem is not new. Something about the way Xcode infers library linkage dependencies has, for several years at least, led it to count dependencies from Swift packages separately from each package, and to subsequently pass the pertinent “-l” parameter to the linker redundantly for each such dependency. In other words, if you have three Swift packages that each requires “-lc++”, Xcode generates a linker line that literally passes “-lc++ -lc++ -lc++” to the linker.

So why have the warnings only appeared now? One of the major changes in Xcode 15 was the introduction of a “completely rewritten linker”, which has been mostly transparent to me, but which was also to blame for an issue with Xcode 15 that prevented some apps from launching on older macOS (10.12) and iOS (14) systems. This bug has been addressed in the Xcode 15.1 beta 1 release, which was just released this week.

Another change in the new linker was the introduction of a new warning flag, on by default: “-warn_duplicate_libraries”. Kinda obvious, isn’t it? It’s the new warning flag on the linker that is leading this old Xcode behavior to suddenly start spewing unwanted warnings.

Suppressing the Warnings

Luckily, in conjunction with the new warning flag, the linker also introduced a negating flag: “-no_warn_duplicate_libraries”. If you pass this flag to the linker, the warnings go away. When Xcode links your target, it actually invokes “clang”, which in turn invokes the linker (“ld”). So to see that the flag gets passed down to the lower lever linker, you need to specify it as a parameter to clang that instructs it to propagate the parameter to ld:

OTHER_LDFLAGS = -Wl,-no_warn_duplicate_libraries

OTHER_LDFLAGS is the raw name for the build setting that appears in Xcode as “Other Linker Flags”. The excerpt above is in the text-based Xcode configuration file (.xcconfig) format. You could also just paste the “-Wl,-no_warn_duplicate_libraries” argument into the Xcode build settings for your target, but generally speaking, using Xcode configuration files is better.

Supporting Multiple Xcode Versions

One problem with pasting the OTHER_LDFLAGS value in to Xcode’s build settings, is you won’t be able to build the project on Xcode 14 or earlier. Maybe that is OK, but if like me, you use an older version of Xcode on your build machine (or your dedicated continuous integration service does), you’ll run into a build error when the older linker fails to recognize the new option:

ld: unknown option: -no_warn_duplicate_libraries

So how can we suppress the warnings while using Xcode 15, while continuing to build without errors on older versions of Xcode? Xcode configuration files to the rescue! Because of the general utility of the method used to conditionalize build settings based on Xcode version, I have written a separate blog post describing this technique. Read Conditional Xcode Build Settings to learn more.

Addressing the Underlying Issue

I’m not going to fret too much about disabling the “warn_duplicate_libraries” option on the linker, because I don’t foresee it protecting me from many real-world pitfalls. However, all else being equal I would leave the warning on, because you never know.

I filed FB13229994 with a sample project demonstrating the problem. Hopefully at some point in the future Apple will update Xcode so that it effectively removes duplicated library names from its list of derived dependencies, alleviating the problem and allowing the warning to be enabled again.