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