Skip to content

Commit 0b191ba

Browse files
authored
Add shared Darwin implementation for plugins (flutter#176495)
This PR introduces support for creating Flutter plugins with a single, shared implementation for iOS and macOS. This is enabled by a new darwin platform option in the flutter create command, which simplifies code maintenance and reduces duplication for plugin authors. *List which issues are fixed by this PR. You must list at least one issue. An issue is not required if the PR fixes something trivial like a typo.* Fixes flutter#161019 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## 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]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- 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 9655042 commit 0b191ba

File tree

18 files changed

+685
-31
lines changed

18 files changed

+685
-31
lines changed

dev/devicelab/bin/tasks/plugin_test_ios.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Future<void> main() async {
1919
PluginTest('ios', <String>['--platforms=ios,macos'], sharedDarwinSource: true).call,
2020
// Test that FFI plugins are supported.
2121
PluginTest('ios', <String>['--platforms=ios'], template: 'plugin_ffi').call,
22+
// Test that plugins support the darwin platform.
23+
PluginTest('ios', <String>['--platforms=darwin']).call,
2224
]),
2325
);
2426
}

dev/devicelab/bin/tasks/plugin_test_macos.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Future<void> main() async {
1515
PluginTest('macos', <String>['--platforms=ios,macos'], sharedDarwinSource: true).call,
1616
// Test that FFI plugins are supported.
1717
PluginTest('macos', <String>['--platforms=macos'], template: 'plugin_ffi').call,
18+
// Test that plugins support the darwin platform.
19+
PluginTest('macos', <String>['--platforms=darwin']).call,
1820
]),
1921
);
2022
}

dev/devicelab/lib/tasks/plugin_tests.dart

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ public class $pluginClass: NSObject, FlutterPlugin {
427427
'--template=$template',
428428
'--org',
429429
'io.flutter.devicelab',
430-
...options,
430+
if (template == 'app' && isDarwin) '--platforms=$target' else ...options,
431431
name,
432432
],
433433
environment: environment,
@@ -490,35 +490,44 @@ end
490490
// Make the platform version artificially low to test that the "deployment
491491
// version too low" warning is never emitted.
492492
void _reduceDarwinPluginMinimumVersion(String plugin, String target) {
493-
final podspec = File(path.join(rootPath, target, '$plugin.podspec'));
493+
var podspec = File(path.join(rootPath, target, '$plugin.podspec'));
494+
if (!podspec.existsSync()) {
495+
// Fallback to darwin directory for shared darwin plugins
496+
podspec = File(path.join(rootPath, 'darwin', '$plugin.podspec'));
497+
}
494498
if (!podspec.existsSync()) {
495499
throw TaskResult.failure('podspec file missing at ${podspec.path}');
496500
}
497-
final versionString = target == 'ios'
498-
? "s.platform = :ios, '13.0'"
499-
: "s.platform = :osx, '10.11'";
500501
String podspecContent = podspec.readAsStringSync();
502+
final bool isMultiPlatform = podspecContent.contains('s.osx.deployment_target');
503+
final String versionString;
504+
if (isMultiPlatform) {
505+
versionString = target == 'ios'
506+
? "s.ios.deployment_target = '13.0'"
507+
: "s.osx.deployment_target = '10.15'";
508+
} else {
509+
versionString = target == 'ios' ? "s.platform = :ios, '13.0'" : "s.platform = :osx, '10.11'";
510+
}
511+
501512
if (!podspecContent.contains(versionString)) {
502513
throw TaskResult.failure(
503514
'Update this test to match plugin minimum $target deployment version',
504515
);
505516
}
506517
// Add transitive dependency on AppAuth 1.6 targeting iOS 8 and macOS 10.9, which no longer builds in Xcode
507518
// to test the version is forced higher and builds.
508-
const iosContent = '''
509-
s.platform = :ios, '10.0'
510-
s.dependency 'AppAuth', '1.6.0'
511-
''';
512-
513-
const macosContent = '''
514-
s.platform = :osx, '10.8'
515-
s.dependency 'AppAuth', '1.6.0'
516-
''';
519+
final String replacementContent;
520+
if (isMultiPlatform) {
521+
replacementContent = target == 'ios'
522+
? "s.ios.deployment_target = '10.0'\n s.dependency 'AppAuth', '1.6.0'"
523+
: "s.osx.deployment_target = '10.8'\n s.dependency 'AppAuth', '1.6.0'";
524+
} else {
525+
replacementContent = target == 'ios'
526+
? "s.platform = :ios, '10.0'\ns.dependency 'AppAuth', '1.6.0'"
527+
: "s.platform = :osx, '10.8'\ns.dependency 'AppAuth', '1.6.0'";
528+
}
517529

518-
podspecContent = podspecContent.replaceFirst(
519-
versionString,
520-
target == 'ios' ? iosContent : macosContent,
521-
);
530+
podspecContent = podspecContent.replaceFirst(versionString, replacementContent);
522531
podspec.writeAsStringSync(podspecContent, flush: true);
523532
}
524533

packages/flutter_tools/lib/src/commands/create.dart

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,13 @@ class CreateCommand extends FlutterCommand with CreateBase {
104104
'internally and should generally not be used manually.',
105105
hide: !verboseHelp,
106106
);
107-
addPlatformsOptions(customHelp: kPlatformHelp);
107+
108+
final Map<String, String> platformsAllowedHelp = {
109+
for (final String p in kAllCreatePlatforms) p: '',
110+
};
111+
platformsAllowedHelp['darwin'] =
112+
'A shared platform for iOS and macOS. (only supported for plugins)';
113+
addPlatformsOptions(customHelp: kPlatformHelp, allowedHelp: platformsAllowedHelp);
108114

109115
final List<ParsedFlutterTemplateType> enabledTemplates =
110116
ParsedFlutterTemplateType.enabledValues(featureFlags);
@@ -356,6 +362,7 @@ class CreateCommand extends FlutterCommand with CreateBase {
356362

357363
final String dartSdk = globals.cache.dartSdkBuild;
358364
final bool includeIos;
365+
final bool includeDarwin;
359366
final bool includeAndroid;
360367
final bool includeWeb;
361368
final bool includeLinux;
@@ -369,6 +376,7 @@ class CreateCommand extends FlutterCommand with CreateBase {
369376
includeLinux = false;
370377
includeMacos = false;
371378
includeWindows = false;
379+
includeDarwin = false;
372380
} else if (template == FlutterTemplateType.package) {
373381
// The package template does not supports any platform.
374382
includeIos = false;
@@ -377,12 +385,32 @@ class CreateCommand extends FlutterCommand with CreateBase {
377385
includeLinux = false;
378386
includeMacos = false;
379387
includeWindows = false;
388+
includeDarwin = false;
380389
} else {
381-
includeIos = featureFlags.isIOSEnabled && platforms.contains('ios');
390+
final bool darwinRequested = platforms.contains('darwin');
391+
final bool darwinSupported =
392+
(template == FlutterTemplateType.plugin) &&
393+
featureFlags.isIOSEnabled &&
394+
featureFlags.isMacOSEnabled;
395+
396+
if (darwinRequested && darwinSupported) {
397+
includeDarwin = true;
398+
includeIos = true;
399+
includeMacos = true;
400+
} else {
401+
includeDarwin = false;
402+
includeIos = featureFlags.isIOSEnabled && platforms.contains('ios');
403+
includeMacos = featureFlags.isMacOSEnabled && platforms.contains('macos');
404+
if (darwinRequested && !darwinSupported) {
405+
globals.printWarning(
406+
'Warning: To use the "darwin" platform, you must have both iOS and macOS enabled.\n'
407+
'Run "flutter config --enable-ios --enable-macos-desktop" and try again.',
408+
);
409+
}
410+
}
382411
includeAndroid = featureFlags.isAndroidEnabled && platforms.contains('android');
383412
includeWeb = featureFlags.isWebEnabled && platforms.contains('web');
384413
includeLinux = featureFlags.isLinuxEnabled && platforms.contains('linux');
385-
includeMacos = featureFlags.isMacOSEnabled && platforms.contains('macos');
386414
includeWindows = featureFlags.isWindowsEnabled && platforms.contains('windows');
387415
}
388416

@@ -418,6 +446,7 @@ class CreateCommand extends FlutterCommand with CreateBase {
418446
iosDevelopmentTeam: developmentTeam,
419447
ios: includeIos,
420448
android: includeAndroid,
449+
darwin: includeDarwin,
421450
web: includeWeb,
422451
linux: includeLinux,
423452
macos: includeMacos,
@@ -529,9 +558,9 @@ class CreateCommand extends FlutterCommand with CreateBase {
529558
await project.ensureReadyForPlatformSpecificTooling(
530559
releaseMode: ignoreReleaseModeSinceItsNotABuildAndHopeItWorks,
531560
androidPlatform: includeAndroid,
532-
iosPlatform: includeIos,
561+
iosPlatform: includeIos || includeDarwin,
533562
linuxPlatform: includeLinux,
534-
macOSPlatform: includeMacos,
563+
macOSPlatform: includeMacos || includeDarwin,
535564
windowsPlatform: includeWindows,
536565
webPlatform: includeWeb,
537566
);
@@ -697,10 +726,28 @@ Your $application code is in $relativeAppMain.
697726
templateContext['description'] = description;
698727

699728
final projectName = templateContext['projectName'] as String?;
729+
final bool includeDarwin = templateContext['darwin'] as bool? ?? false;
730+
final bool originalIos = templateContext['ios'] as bool? ?? false;
731+
final bool originalMacos = templateContext['macos'] as bool? ?? false;
732+
if (includeDarwin) {
733+
// Temporarily disable ios/macos for the plugin generation
734+
// so we don't get ios/ and macos/ directories in the plugin root.
735+
templateContext['ios'] = false;
736+
templateContext['macos'] = false;
737+
}
738+
700739
final templates = <String>['plugin', 'plugin_shared'];
701-
if ((templateContext['ios'] == true || templateContext['macos'] == true) &&
702-
featureFlags.isSwiftPackageManagerEnabled) {
703-
templates.add('plugin_swift_package_manager');
740+
741+
final bool useSwiftPackageManager =
742+
(templateContext['ios'] == true || templateContext['macos'] == true || includeDarwin) &&
743+
featureFlags.isSwiftPackageManagerEnabled;
744+
745+
if (useSwiftPackageManager) {
746+
if (includeDarwin) {
747+
templates.add('plugin_darwin_spm');
748+
} else {
749+
templates.add('plugin_swift_package_manager');
750+
}
704751
templateContext['swiftLibraryName'] = projectName?.replaceAll('_', '-');
705752
templateContext['swiftToolsVersion'] = minimumSwiftToolchainVersion;
706753
templateContext['iosSupportedPlatform'] = FlutterDarwinPlatform.ios.supportedPackagePlatform
@@ -710,16 +757,24 @@ Your $application code is in $relativeAppMain.
710757
.supportedPackagePlatform
711758
.format();
712759
} else {
713-
templates.add('plugin_cocoapods');
760+
if (includeDarwin) {
761+
templates.add('plugin_darwin_cocoapods');
762+
} else {
763+
templates.add('plugin_cocoapods');
764+
}
714765
}
715-
716766
generatedCount += await renderMerged(
717767
templates,
718768
directory,
719769
templateContext,
720770
overwrite: overwrite,
721771
printStatusWhenWriting: printStatusWhenWriting,
722772
);
773+
// Restore the original ios and macos values.
774+
// This is necessary in case the user requested the darwin platform,
775+
// and we need to restore them for the example app generation.
776+
templateContext['ios'] = originalIos;
777+
templateContext['macos'] = originalMacos;
723778

724779
final FlutterProject project = FlutterProject.fromDirectory(directory);
725780
final generateAndroid = templateContext['android'] == true;
@@ -984,6 +1039,10 @@ List<String> _getPlatformWarningList(List<String> requestedPlatforms) {
9841039
if (requestedPlatforms.contains('macos') && !featureFlags.isMacOSEnabled) 'macos',
9851040
if (requestedPlatforms.contains('windows') && !featureFlags.isWindowsEnabled) 'windows',
9861041
if (requestedPlatforms.contains('linux') && !featureFlags.isLinuxEnabled) 'linux',
1042+
if (requestedPlatforms.contains('darwin') &&
1043+
!featureFlags.isMacOSEnabled &&
1044+
!featureFlags.isIOSEnabled)
1045+
'darwin',
9871046
];
9881047

9891048
return platformsToWarn;
@@ -992,13 +1051,16 @@ List<String> _getPlatformWarningList(List<String> requestedPlatforms) {
9921051
void _printWarningDisabledPlatform(List<String> platforms) {
9931052
final desktop = <String>[];
9941053
final web = <String>[];
1054+
final darwin = <String>[];
9951055

9961056
for (final platform in platforms) {
9971057
switch (platform) {
9981058
case 'web':
9991059
web.add(platform);
10001060
case 'macos' || 'windows' || 'linux':
10011061
desktop.add(platform);
1062+
case 'darwin':
1063+
darwin.add(platform);
10021064
}
10031065
}
10041066

@@ -1015,6 +1077,12 @@ For more details, see: https://flutter.dev/to/add-desktop-support
10151077
globals.printStatus('''
10161078
The web is currently not supported on your local environment.
10171079
For more details, see: https://flutter.dev/to/add-web-support
1080+
''');
1081+
}
1082+
if (darwin.isNotEmpty) {
1083+
globals.printStatus('''
1084+
The darwin platform is currently not supported on your local environment.
1085+
You must have a macOS host with Xcode installed to develop for iOS or macOS.
10181086
''');
10191087
}
10201088
}

packages/flutter_tools/lib/src/commands/create_base.dart

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,15 @@ const _kAvailablePlatforms = <String>['ios', 'android', 'windows', 'linux', 'mac
2222

2323
/// A list of all possible create platforms, even those that may not be enabled
2424
/// with the current config.
25-
const kAllCreatePlatforms = <String>['ios', 'android', 'windows', 'linux', 'macos', 'web'];
25+
const kAllCreatePlatforms = <String>[
26+
'ios',
27+
'android',
28+
'windows',
29+
'linux',
30+
'macos',
31+
'web',
32+
'darwin',
33+
];
2634

2735
const _kDefaultPlatformArgumentHelp =
2836
'(required) The platforms supported by this project. '
@@ -94,13 +102,14 @@ mixin CreateBase on FlutterCommand {
94102
///
95103
/// The help message of the argument is replaced with `customHelp` if `customHelp` is not null.
96104
@protected
97-
void addPlatformsOptions({String? customHelp}) {
105+
void addPlatformsOptions({String? customHelp, required Map<String, String> allowedHelp}) {
98106
argParser.addMultiOption(
99107
'platforms',
100108
help: customHelp ?? _kDefaultPlatformArgumentHelp,
101109
aliases: <String>['platform'],
102110
defaultsTo: <String>[..._kAvailablePlatforms],
103-
allowed: <String>[..._kAvailablePlatforms],
111+
allowed: <String>[...kAllCreatePlatforms],
112+
allowedHelp: allowedHelp,
104113
);
105114
}
106115

@@ -312,6 +321,7 @@ mixin CreateBase on FlutterCommand {
312321
bool linux = false,
313322
bool macos = false,
314323
bool windows = false,
324+
bool darwin = false,
315325
bool implementationTests = false,
316326
}) {
317327
final String pluginDartClass = _createPluginClassName(projectName);
@@ -336,6 +346,7 @@ mixin CreateBase on FlutterCommand {
336346
'androidIdentifier': androidIdentifier,
337347
'iosIdentifier': appleIdentifier,
338348
'macosIdentifier': appleIdentifier,
349+
'darwinIdentifier': appleIdentifier,
339350
'linuxIdentifier': linuxIdentifier,
340351
'windowsIdentifier': windowsIdentifier,
341352
'description': projectDescription,
@@ -365,6 +376,8 @@ mixin CreateBase on FlutterCommand {
365376
'web': web,
366377
'linux': linux,
367378
'macos': macos,
379+
'darwin': darwin,
380+
'sharedDarwinSource': darwin,
368381
'windows': windows,
369382
'year': DateTime.now().year,
370383
'dartSdkVersionBounds': dartSdkVersionBounds,
@@ -469,6 +482,7 @@ mixin CreateBase on FlutterCommand {
469482
final bool macOSPlatform = templateContext['macos'] as bool? ?? false;
470483
final bool windowsPlatform = templateContext['windows'] as bool? ?? false;
471484
final bool webPlatform = templateContext['web'] as bool? ?? false;
485+
final bool darwinPlatform = templateContext['darwin'] as bool? ?? false;
472486

473487
final platformsForMigrateConfig = <SupportedPlatform>[SupportedPlatform.root];
474488
if (androidPlatform) {
@@ -484,6 +498,14 @@ mixin CreateBase on FlutterCommand {
484498
if (macOSPlatform) {
485499
platformsForMigrateConfig.add(SupportedPlatform.macos);
486500
}
501+
if (darwinPlatform) {
502+
if (!platformsForMigrateConfig.contains(SupportedPlatform.ios)) {
503+
platformsForMigrateConfig.add(SupportedPlatform.ios);
504+
}
505+
if (!platformsForMigrateConfig.contains(SupportedPlatform.macos)) {
506+
platformsForMigrateConfig.add(SupportedPlatform.macos);
507+
}
508+
}
487509
if (webPlatform) {
488510
platformsForMigrateConfig.add(SupportedPlatform.web);
489511
}

0 commit comments

Comments
 (0)