1# A primer on macOS SDKs 2 3## Overview 4 5A macOS SDK is an on-disk directory that contains header files and meta information for macOS APIs. 6Apple distributes SDKs as part of the Xcode app bundle. Each Xcode version comes with one macOS SDK, 7the SDK for the most recent released version of macOS at the time of the Xcode release. 8The SDK is located at `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk`. 9 10Compiling Firefox for macOS requires a macOS SDK. The build system uses the SDK from Xcode.app by 11default, and you can select a different SDK using the `mozconfig` option `--with-macos-sdk`: 12 13```text 14ac_add_options --with-macos-sdk=/Users/username/SDKs/MacOSX10.12.sdk 15``` 16 17## Supported SDKs 18 19First off, Firefox runs on 10.9 and above. This is called the "minimum deployment target" and is 20independent of the SDK version. 21 22Our official Firefox builds compiled in CI (continuous integration) currently use the 10.12 SDK. 23[Bug 1475652](https://bugzilla.mozilla.org/show_bug.cgi?id=1475652) tracks updating this SDK. 24 25For local builds, all SDKs from 10.12 to 10.15 are supported. Firefox should compile successfully 26with all of those SDKs, but minor differences in runtime behavior can occur. 27 28However, since only the 10.12 SDK is used in CI, compiling with different SDKs breaks from time to time. 29Such breakages should be [reported in Bugzilla](https://bugzilla.mozilla.org/enter_bug.cgi?blocked=mach-busted&bug_type=defect&cc=:spohl,:mstange&component=General&form_name=enter_bug&keywords=regression&op_sys=macOS&product=Firefox%20Build%20System&rep_platform=All) and fixed quickly. 30 31Aside: Firefox seems to be a bit of a special snowflake with its ability to build with an arbitrary SDK. 32For example, at the time of this writing (June 2020), 33[building Chrome requires the 10.15 SDK](https://chromium.googlesource.com/chromium/src/+/master/docs/mac_build_instructions.md#system-requirements). 34Some apps even require a certain version of Xcode and only support building with the SDK of that Xcode version. 35 36Why are we using such an old SDK in CI, you ask? It basically comes down to the fact that macOS 37hardware is expensive, and the fact that the compilers and linkers supplied by Xcode don't run on Linux. 38 39## Obtaining SDKs 40 41Sometimes you need an SDK that's different from the one in your Xcode.app, for example 42to check whether your code change breaks building with other SDKs, or to verify the 43runtime behavior with the SDK used for CI builds. 44 45The easy but slightly questionable way to obtain an SDK is to download it from a public github repo. 46 47Here's another option: 48 49 1. Have your Apple ID login details ready, and bring enough time and patience for a 5GB download. 50 2. Check [these tables in the Xcode wikipedia article](https://en.wikipedia.org/wiki/Xcode#Xcode_7.0_-_10.x_(since_Free_On-Device_Development)) 51 and find an Xcode version that contains the SDK you need. 52 3. Look up the Xcode version number on [xcodereleases.com](https://xcodereleases.com/) and click the Download link for it. 53 4. Log in with your Apple ID. Then the download should start. 54 5. Wait for the 5GB Xcode_*.xip download to finish. 55 6. Open the downloaded xip file. This will extract the Xcode.app bundle. 56 7. Inside the app bundle, the SDK is at `Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk`. 57 58## Effects of the SDK version 59 60An SDK only contains declarations of APIs. It does not contain the implementations for these APIs. 61 62The implementation of an API is provided by the OS that the app runs on. It is supplied at runtime, 63when your app starts up, by the dynamic linker. For example, the AppKit implementation comes 64from `/System/Library/Frameworks/AppKit.framework` from the OS that the app is run on, regardless 65of what SDK was used when compiling the app. 66 67In other words, building with a macOS SDK of a higher version doesn't magically make new APIs available 68when running on older versions of macOS. And, conversely, building with a lower macOS SDK doesn't limit 69which APIs you can use if your app is run on a newer version of macOS, assuming you manage to convince the 70compiler to accept your code. 71 72The SDK used for building an app determines three things: 73 74 1. Whether your code compiles at all, 75 2. which range of macOS versions your app can run on (available deployment targets), and 76 3. certain aspects of runtime behavior. 77 78The first is straightforward: An SDK contains header files. If you call an API that's not declared 79anywhere - neither in a header file nor in your own code - then your compiler will emit an error. 80(Special case: Calling an unknown Objective-C method usually only emits a warning, not an error.) 81 82The second aspect, available deployment targets, is usually not worth worrying about: 83SDKs have large ranges of supported macOS deployment targets. 84For example, the 10.15 SDK supports running your app on macOS versions all the way back to 10.6. 85This information is written down in the SDK's `SDKSettings.plist`. 86 87The third aspect, varying runtime behavior, is perhaps the most insidious and surprising aspect, and is described 88in the next section. 89 90## Runtime differences based on macOS SDK version 91 92When a new version of macOS is released, existing APIs can change their behavior. 93These changes are usually described in the AppKit release notes: 94 95 - [macOS 10.15 release notes](https://developer.apple.com/documentation/macos_release_notes/macos_catalina_10_15_release_notes?language=objc) 96 - [macOS 10.14 AppKit release notes](https://developer.apple.com/documentation/macos_release_notes/macos_mojave_10_14_release_notes/appkit_release_notes_for_macos_10_14?language=objc) 97 - [macOS 10.13 AppKit release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/) 98 - [macOS 10.12 and older AppKit release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKitOlderNotes/) 99 100Sometimes, these differences in behavior have the potential to break existing apps. In those instances, 101Apple often provides the old (compatible) behavior until the app is re-built with the new SDK, expecting 102developers to update their apps so that they work with the new behavior, at the same time as 103they update to the new SDK. 104 105Here's an [example from the 10.13 release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/#10_13NSCollectionView%20Responsive%20Scrolling): 106 107> Responsive Scrolling in NSCollectionViews is enabled only for apps linked on or after macOS 10.13. 108 109Here, "linked on or after macOS 10.13" means "linked against the macOS 10.13 SDK or newer". 110 111Apple's expectation is that you upgrade to the new macOS version when it is released, download a new 112Xcode version when it is released, synchronize these updates across the machines of all developers 113that work on your app, use the SDK in the newest Xcode to compile your app, and make changes to your 114app to be compatible with any behavior changes whenever you update Xcode. 115This expectation does not always match reality. It definitely doesn't match what we're doing with Firefox. 116 117For Firefox, SDK-dependent compatibility behaviors mean that developers who build Firefox locally 118can see different runtime behavior than the users of our CI builds, if they use a different SDK than 119the SDK used in CI. 120That is, unless we change the Firefox code so that it has the same behavior regardless of SDK version. 121Often this can be achieved by using APIs in a way that's more in line with the API's recommended use. 122 123For example, we've had cases of 124[broken placeholder text in search fields](https://bugzilla.mozilla.org/show_bug.cgi?id=1273106), 125[missing](https://bugzilla.mozilla.org/show_bug.cgi?id=941325) or [double-drawn focus rings](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/nsNativeThemeCocoa.mm#149-169), 126[a startup crash](https://bugzilla.mozilla.org/show_bug.cgi?id=1516437), 127[fully black windows](https://bugzilla.mozilla.org/show_bug.cgi?id=1494022), 128[fully gray windows](https://bugzilla.mozilla.org/show_bug.cgi?id=1576113#c4), 129[broken vibrancy](https://bugzilla.mozilla.org/show_bug.cgi?id=1475694), and 130[broken colors in dark mode](https://bugzilla.mozilla.org/show_bug.cgi?id=1578917). 131 132In most of these cases, the breakage was either very minor, or it was caused by Firefox doing things 133that were explicitly discouraged, like creating unexpected NSView hierarchies, or relying on unspecified 134implementation details. (With one exception: In 10.14, HiDPI-aware `NSOpenGLContext` rendering in 135layer-backed windows simply broke.) 136 137And in all of these cases, it was the SDK-dependent compatibility behavior that protected our users from being 138exposed to the breakage. Our CI builds continued to work because they were built with an older SDK. 139 140We have addressed all known cases of breakage when building Firefox with newer SDKs. 141I am not aware of any current instances of this problem as of this writing (June 2020). 142 143For more information about how these compatibility tricks work, 144read the [Overriding SDK-dependent runtime behavior](#overriding-sdk-dependent-runtime-behavior) section. 145 146## Supporting multiple SDKs 147 148As described under [Supported SDKs](#supported-sdks), Firefox can be built with a wide variety of SDK versions. 149 150This ability comes at the cost of some manual labor; it requires some well-placed `#ifdefs` and 151copying of header definitions. 152 153Every SDK defines the macro `MAC_OS_X_VERSION_MAX_ALLOWED` with a value that matches the SDK version, 154in the SDK's `AvailabilityMacros.h` header. This header also defines version constants like `MAC_OS_X_VERSION_10_12`. 155For example, I have a version of the 10.12 SDK which contains the line 156 157```cpp 158#define MAC_OS_X_VERSION_MAX_ALLOWED MAC_OS_X_VERSION_10_12_4 159``` 160 161The name `MAC_OS_X_VERSION_MAX_ALLOWED` is rather misleading; a better name would be 162`MAC_OS_X_VERSION_MAX_KNOWN_BY_SDK`. Compiling with an old SDK *does not* prevent apps from running 163on newer versions of macOS. 164 165With the help of the `MAC_OS_X_VERSION_MAX_ALLOWED` macro, we can make our code adapt to the SDK that's 166being used. Here's [an example](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/toolkit/xre/MacApplicationDelegate.mm#345-351) where the 10.14 SDK changed the signature of 167[an `NSApplicationDelegate` method](https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428471-application?language=objc): 168 169```objc++ 170- (BOOL)application:(NSApplication*)application 171 continueUserActivity:(NSUserActivity*)userActivity 172#if defined(MAC_OS_X_VERSION_10_14) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14 173 restorationHandler:(void (^)(NSArray<id<NSUserActivityRestoring>>*))restorationHandler { 174#else 175 restorationHandler:(void (^)(NSArray*))restorationHandler { 176#endif 177 ... 178} 179``` 180 181We can also use this macro to supply missing API definitions in such a way that 182they don't conflict with the definitions from the SDK. 183This is described in the "Using macOS APIs" document, under [Using new APIs with old SDKs](./macos-apis.html#using-new-apis-with-old-sdks). 184 185## Overriding SDK-dependent runtime behavior 186 187This section contains some more details on the compatibility tricks that cause different runtime 188behavior dependent on the SDK, as described in 189[Runtime differences based on macOS SDK version](#runtime-differences-based-on-macos-sdk-version). 190 191### How it works 192 193AppKit is the one system framework I know of that employs these tricks. Let's explore how AppKit makes this work, 194by going back to the [NSCollectionView example](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/#10_13NSCollectionView%20Responsive%20Scrolling) from above: 195 196> Responsive Scrolling in NSCollectionViews is enabled only for apps linked on or after macOS 10.13. 197 198For each of these SDK-dependent behavior differences, both the old and the new behavior are implemented 199in the version of AppKit that ships with the new macOS version. 200At runtime, AppKit selects one of the behaviors based on the SDK version, with a call to 201`_CFExecutableLinkedOnOrAfter()`. This call checks the SDK version of the main executable of the 202process that's running AppKit code; in our case that's the `firefox` or `plugin-container` executable. 203The SDK version is stored in the mach-o headers of the executable by the linker. 204 205One interesting design aspect of AppKit's compatibility tricks is the fact that most of these behavior differences 206can be toggled with a "user default" preference. 207For example, the "responsive scrolling in NSCollectionViews" behavior change can be controlled with 208a user default with the name "NSCollectionViewPrefetchingEnabled". 209The SDK check only happens if "NSCollectionViewPrefetchingEnabled" is not set to either YES or NO. 210 211More precisely, this example works as follows: 212 213 - `-[NSCollectionView prepareContentInRect:]` is the function that supports both the old and the new behavior. 214 - It calls `_NSGetBoolAppConfig` for the value "NSCollectionViewPrefetchingEnabled", and also supplies a "default 215 value function". 216 - If the user default is not set, the default value function is called. This function has the name 217 `NSCollectionViewPrefetchingEnabledDefaultValueFunction`. 218 - `NSCollectionViewPrefetchingEnabledDefaultValueFunction` calls `_CFExecutableLinkedOnOrAfter(13)`. 219 220You can find many similar toggles if you list the AppKit symbols that end in `DefaultValueFunction`, 221for example by executing `nm /System/Library/Frameworks/AppKit.framework/AppKit | grep DefaultValueFunction`. 222 223### Overriding SDK-dependent runtime behavior 224 225You can set these preferences programmatically, in a way that `_NSGetBoolAppConfig()` can pick them up, 226for example with [`registerDefaults`](https://developer.apple.com/documentation/foundation/nsuserdefaults/1417065-registerdefaults?language=objc) 227or like this: 228 229```objc++ 230[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NSViewAllowsRootLayerBacking"]; 231``` 232 233The AppKit release notes mention this ability but ask for it to only be used for debugging purposes: 234 235> In some cases, we provide defaults (preferences) settings which can be used to get the old or new behavior, 236> independent of what system an application was built against. Often these preferences are provided for 237> debugging purposes only; in some cases the preferences can be used to globally modify the behavior 238> of an application by registering the values (do it somewhere very early, with `-[NSUserDefaults registerDefaults:]`). 239 240It's interesting that they mention this at all because, as far as I can tell, none of these values are documented. 241