Skip to content

Commit 975a677

Browse files
authored
Make iOS Flutter framework extension-safe (#165346)
Our current [adoption/documentation](https://docs.flutter.dev/platform-integration/ios/app-extensions) for iOS Extensions does not currently work because it's disallowed to nest bundles (see flutter/flutter#142531). As of [Xcode 13](https://developer.apple.com/documentation/xcode-release-notes/xcode-13-release-notes/#:~:text=Linking%20Swift%20packages%20from%20application%20extension%20targets%20or%20watchOS%20applications%20no%20longer%20emits%20unresolvable%20warnings%20about%20linking%20to%20libraries%20not%20safe%20for%20use%20in%20application%20extensions%2E), linking to frameworks that contain non-extension-safe code no longer gives warnings (or blocks from App Store it seems). Therefore, it has become a runtime issue to ensure non-extension-safe code is not used. Previously, we were building and shipping 2 separate Flutter.xcframeworks. One that was extension-safe and one that was not. This PR removes the "extension_safe" framework and instead makes the entire framework extension-safe by annotating non-extension-safe code with `NS_EXTENSION_UNAVAILABLE_IOS` and ensuring that code is not used at runtime if the bundle is for an app extension. This PR also disables wide gamut for app extensions to decrease the chances of crashes (see flutter/flutter#165086). Fixes flutter/flutter#142531. --- For reference: App extensions were first evaluated in https://flutter.dev/go/app-extensions. Here is the reasoning why neither method described there is opportune. Option 1 - I did look into splitting the framework into 2 frameworks (one with all extension-safe code and one with the non-extension-safe code). However, the original idea was to use objc Categories/Extensions - this doesn’t quite fit our needs. Categories/Extensions only allow you to add new methods/properties, not override existing ones. We could hypothetically add new methods, but that would require the user to change their code to use the new methods. I also looked into subclasses which does allow overrides, but it would also require the user to change their code to use the new class. We could do method swizzling, but opinion of that on the internet is that it's not very safe. I’m of the opinion that anything that requires the user to change code isn’t super feasible due to plugins. Option 2 - We could still do the 2 frameworks but rename one to `FlutterExtentionSafe`. This works without users needing to change any code (including imports like `@import Flutter` / `#import <Flutter/Flutter.h>`). I believe the reason this works is because at compile time, it finds the `Flutter` framework on the framework search path and it imports in the headers. Then at link time, `FlutterExtentionSafe` is explicitly linked so it uses that framework first when checking for symbols and since it finds all the symbols in `FlutterExtentionSafe`, it doesn’t need/try to auto-link the `Flutter` framework (despite `Flutter` being the framework imported). This seems precarious to me since we’re relying on Xcode to not auto-link the `Flutter` framework. If for some reason `Flutter` framework did get auto-linked (such as the user using a symbol that’s not in the `FlutterExtensionSafe` framework but is in the `Flutter` framework - this is unlikely though), we’d get name collision issues ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 3efb8cc commit 975a677

20 files changed

+794
-274
lines changed

engine/src/build/config/ios/BUILD.gn

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@
55
config("sdk") {
66
cflags_cc = [ "-stdlib=libc++" ]
77
}
8+
9+
config("ios_application_extension") {
10+
cflags_objc = [ "-fapplication-extension" ]
11+
cflags_objcc = [ "-fapplication-extension" ]
12+
ldflags = [ "-fapplication-extension" ]
13+
}

engine/src/flutter/ci/licenses_golden/licenses_flutter

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52788,6 +52788,9 @@ ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterResto
5278852788
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm + ../../../flutter/LICENSE
5278952789
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h + ../../../flutter/LICENSE
5279052790
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm + ../../../flutter/LICENSE
52791+
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h + ../../../flutter/LICENSE
52792+
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.mm + ../../../flutter/LICENSE
52793+
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplicationTest.mm + ../../../flutter/LICENSE
5279152794
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h + ../../../flutter/LICENSE
5279252795
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm + ../../../flutter/LICENSE
5279352796
ORIGIN: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPluginTest.mm + ../../../flutter/LICENSE
@@ -55789,6 +55792,9 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestora
5578955792
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm
5579055793
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.h
5579155794
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSemanticsScrollView.mm
55795+
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h
55796+
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.mm
55797+
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplicationTest.mm
5579255798
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h
5579355799
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.mm
5579455800
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPluginTest.mm

engine/src/flutter/common/config.gni

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,6 @@ declare_args() {
2323
# Whether to include backtrace support.
2424
enable_backtrace = true
2525

26-
# Whether to include --fapplication-extension when build iOS framework.
27-
# This is currently a test flag and does not work properly.
28-
#TODO(cyanglaz): Remove above comment about test flag when the entire iOS embedder supports app extension
29-
#https://github.com/flutter/flutter/issues/124289
30-
darwin_extension_safe = false
31-
3226
# Whether binary size optimizations with the assumption that Impeller is the
3327
# only supported rendering engine are enabled.
3428
#

engine/src/flutter/shell/platform/darwin/ios/BUILD.gn

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,13 @@ source_set("flutter_framework_source") {
4545
cflags_objcc = flutter_cflags_objcc
4646

4747
defines = [ "FLUTTER_FRAMEWORK=1" ]
48-
if (darwin_extension_safe) {
49-
defines += [ "APPLICATION_EXTENSION_API_ONLY=1" ]
50-
}
5148
public_configs = [
5249
":ios_gpu_configuration_config",
5350
"//flutter:config",
5451
]
5552

53+
configs += [ "//build/config/ios:ios_application_extension" ]
54+
5655
sources = [
5756
"framework/Source/FlutterAppDelegate.mm",
5857
"framework/Source/FlutterCallbackCache.mm",
@@ -88,6 +87,8 @@ source_set("flutter_framework_source") {
8887
"framework/Source/FlutterRestorationPlugin.mm",
8988
"framework/Source/FlutterSemanticsScrollView.h",
9089
"framework/Source/FlutterSemanticsScrollView.mm",
90+
"framework/Source/FlutterSharedApplication.h",
91+
"framework/Source/FlutterSharedApplication.mm",
9192
"framework/Source/FlutterSpellCheckPlugin.h",
9293
"framework/Source/FlutterSpellCheckPlugin.mm",
9394
"framework/Source/FlutterTextInputDelegate.h",
@@ -225,6 +226,7 @@ shared_library("ios_test_flutter") {
225226
"framework/Source/FlutterPlatformViewsTest.mm",
226227
"framework/Source/FlutterPluginAppLifeCycleDelegateTest.mm",
227228
"framework/Source/FlutterRestorationPluginTest.mm",
229+
"framework/Source/FlutterSharedApplicationTest.mm",
228230
"framework/Source/FlutterSpellCheckPluginTest.mm",
229231
"framework/Source/FlutterTextInputPluginTest.mm",
230232
"framework/Source/FlutterTextureRegistryRelayTest.mm",
@@ -262,10 +264,6 @@ shared_library("ios_test_flutter") {
262264
":ios_gpu_configuration_config",
263265
"//flutter:config",
264266
]
265-
266-
if (darwin_extension_safe) {
267-
defines = [ "APPLICATION_EXTENSION_API_ONLY=1" ]
268-
}
269267
}
270268

271269
shared_library("create_flutter_framework_dylib") {
@@ -275,15 +273,12 @@ shared_library("create_flutter_framework_dylib") {
275273

276274
ldflags = [ "-Wl,-install_name,@rpath/Flutter.framework/Flutter" ]
277275

278-
if (darwin_extension_safe) {
279-
ldflags += [ "-fapplication-extension" ]
280-
}
281-
282276
public = _flutter_framework_headers
283277

284278
deps = [ ":flutter_framework_source" ]
285279

286280
public_configs = [ "//flutter:config" ]
281+
configs += [ "//build/config/ios:ios_application_extension" ]
287282
}
288283

289284
copy("copy_dylib") {
@@ -375,9 +370,11 @@ copy("copy_license") {
375370
shared_library("copy_and_verify_framework_module") {
376371
framework_search_path = rebase_path("$root_out_dir")
377372
visibility = [ ":*" ]
378-
cflags_objc = [
373+
cflags_objc = [ "-F$framework_search_path" ]
374+
ldflags = [
379375
"-F$framework_search_path",
380-
"-fapplication-extension",
376+
"-Xlinker",
377+
"-fatal_warnings",
381378
]
382379

383380
sources = [ "framework/Source/FlutterUmbrellaImport.m" ]
@@ -388,16 +385,7 @@ shared_library("copy_and_verify_framework_module") {
388385
":copy_framework_privacy_manifest",
389386
]
390387

391-
if (darwin_extension_safe) {
392-
ldflags = [
393-
"-F$framework_search_path",
394-
"-fapplication-extension",
395-
"-Xlinker",
396-
"-fatal_warnings",
397-
]
398-
deps += [ ":copy_dylib" ]
399-
frameworks = [ "Flutter.framework" ]
400-
}
388+
configs += [ "//build/config/ios:ios_application_extension" ]
401389
}
402390

403391
group("universal_flutter_framework") {

engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Test.h"
1111
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h"
1212
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate_internal.h"
13+
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h"
1314
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
1415

1516
FLUTTER_ASSERT_ARC
@@ -154,6 +155,10 @@ - (BOOL)application:(UIApplication*)application
154155
- (BOOL)handleOpenURL:(NSURL*)url
155156
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options
156157
relayToSystemIfUnhandled:(BOOL)throwBack {
158+
UIApplication* flutterApplication = FlutterSharedApplication.application;
159+
if (flutterApplication == nil) {
160+
return NO;
161+
}
157162
if (![self isFlutterDeepLinkingEnabled]) {
158163
return NO;
159164
}
@@ -164,9 +169,9 @@ - (BOOL)handleOpenURL:(NSURL*)url
164169
completionHandler:^(BOOL success) {
165170
if (!success && throwBack) {
166171
// throw it back to iOS
167-
[UIApplication.sharedApplication openURL:url
168-
options:@{}
169-
completionHandler:nil];
172+
[flutterApplication openURL:url
173+
options:@{}
174+
completionHandler:nil];
170175
}
171176
}];
172177
} else {

engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartVMServicePublisher.h"
2727
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterIndirectScribbleDelegate.h"
2828
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h"
29+
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h"
2930
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSpellCheckPlugin.h"
3031
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h"
3132
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextureRegistryRelay.h"
@@ -227,15 +228,7 @@ - (instancetype)initWithName:(NSString*)labelPrefix
227228
name:UIApplicationDidReceiveMemoryWarningNotification
228229
object:nil];
229230

230-
#if APPLICATION_EXTENSION_API_ONLY
231-
if (@available(iOS 13.0, *)) {
232-
[self setUpSceneLifecycleNotifications:center];
233-
} else {
234-
[self setUpApplicationLifecycleNotifications:center];
235-
}
236-
#else
237-
[self setUpApplicationLifecycleNotifications:center];
238-
#endif
231+
[self setUpLifecycleNotifications:center];
239232

240233
[center addObserver:self
241234
selector:@selector(onLocaleUpdated:)
@@ -250,18 +243,21 @@ + (FlutterEngine*)engineForIdentifier:(int64_t)identifier {
250243
return (__bridge FlutterEngine*)reinterpret_cast<void*>(identifier);
251244
}
252245

253-
- (void)setUpSceneLifecycleNotifications:(NSNotificationCenter*)center API_AVAILABLE(ios(13.0)) {
254-
[center addObserver:self
255-
selector:@selector(sceneWillEnterForeground:)
256-
name:UISceneWillEnterForegroundNotification
257-
object:nil];
258-
[center addObserver:self
259-
selector:@selector(sceneDidEnterBackground:)
260-
name:UISceneDidEnterBackgroundNotification
261-
object:nil];
262-
}
263-
264-
- (void)setUpApplicationLifecycleNotifications:(NSNotificationCenter*)center {
246+
- (void)setUpLifecycleNotifications:(NSNotificationCenter*)center {
247+
// If the application is not available, use the scene for lifecycle notifications if available.
248+
if (!FlutterSharedApplication.isAvailable) {
249+
if (@available(iOS 13.0, *)) {
250+
[center addObserver:self
251+
selector:@selector(sceneWillEnterForeground:)
252+
name:UISceneWillEnterForegroundNotification
253+
object:nil];
254+
[center addObserver:self
255+
selector:@selector(sceneDidEnterBackground:)
256+
name:UISceneDidEnterBackgroundNotification
257+
object:nil];
258+
return;
259+
}
260+
}
265261
[center addObserver:self
266262
selector:@selector(applicationWillEnterForeground:)
267263
name:UIApplicationWillEnterForegroundNotification
@@ -855,19 +851,8 @@ - (BOOL)createShell:(NSString*)entrypoint
855851
_threadHost->io_thread->GetTaskRunner() // io
856852
);
857853

858-
#if APPLICATION_EXTENSION_API_ONLY
859-
if (@available(iOS 13.0, *)) {
860-
_isGpuDisabled = self.viewController.flutterWindowSceneIfViewLoaded.activationState ==
861-
UISceneActivationStateBackground;
862-
} else {
863-
// [UIApplication sharedApplication API is not available for app extension.
864-
// We intialize the shell assuming the GPU is required.
865-
_isGpuDisabled = NO;
866-
}
867-
#else
868-
_isGpuDisabled =
869-
[UIApplication sharedApplication].applicationState == UIApplicationStateBackground;
870-
#endif
854+
// Disable GPU if the app or scene is running in the background.
855+
self.isGpuDisabled = self.viewController.stateIsBackground;
871856

872857
// Create the shell. This is a blocking operation.
873858
std::unique_ptr<flutter::Shell> shell = flutter::Shell::Create(
@@ -1342,23 +1327,21 @@ - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
13421327

13431328
#pragma mark - Notifications
13441329

1345-
#if APPLICATION_EXTENSION_API_ONLY
13461330
- (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
13471331
[self flutterWillEnterForeground:notification];
13481332
}
13491333

13501334
- (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
13511335
[self flutterDidEnterBackground:notification];
13521336
}
1353-
#else
1337+
13541338
- (void)applicationWillEnterForeground:(NSNotification*)notification {
13551339
[self flutterWillEnterForeground:notification];
13561340
}
13571341

13581342
- (void)applicationDidEnterBackground:(NSNotification*)notification {
13591343
[self flutterDidEnterBackground:notification];
13601344
}
1361-
#endif
13621345

13631346
- (void)flutterWillEnterForeground:(NSNotification*)notification {
13641347
[self setIsGpuDisabled:NO];

0 commit comments

Comments
 (0)