From 2df7dca858f3cd05da75f91f596441cc8a5a9b44 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 13 Jan 2022 11:30:22 -0500 Subject: [PATCH] Fully support Dart-only mobile and macOS plugins (#96183) --- dev/devicelab/bin/tasks/module_test_ios.dart | 70 ++++++++- dev/devicelab/bin/tasks/plugin_test.dart | 2 + dev/devicelab/bin/tasks/plugin_test_ios.dart | 3 + dev/devicelab/lib/tasks/plugin_tests.dart | 103 ++++++++++--- packages/flutter_tools/bin/podhelper.rb | 3 +- .../gradle/app_plugin_loader.gradle | 6 + .../gradle/module_plugin_loader.gradle | 6 + .../lib/src/flutter_plugins.dart | 8 +- .../library/Flutter.tmpl/podhelper.rb.tmpl | 3 +- .../test/general.shard/plugins_test.dart | 140 +++++++++++++++++- 10 files changed, 306 insertions(+), 38 deletions(-) diff --git a/dev/devicelab/bin/tasks/module_test_ios.dart b/dev/devicelab/bin/tasks/module_test_ios.dart index ab646eb9d5..81f69bf7bc 100644 --- a/dev/devicelab/bin/tasks/module_test_ios.dart +++ b/dev/devicelab/bin/tasks/module_test_ios.dart @@ -152,14 +152,28 @@ Future main() async { await flutter('clean'); }); + // Make a fake Dart-only plugin, since there are no existing examples. + section('Create local plugin'); + + const String dartPluginName = 'dartplugin'; + await _createFakeDartPlugin(dartPluginName, tempDir); + section('Add plugins'); final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml')); String content = await pubspec.readAsString(); content = content.replaceFirst( '\ndependencies:\n', - // One dynamic framework, one static framework, and one that does not support iOS. - '\ndependencies:\n device_info: 2.0.3\n google_sign_in: 4.5.1\n android_alarm_manager: 0.4.5+11\n', + // One dynamic framework, one static framework, one Dart-only, + // and one that does not support iOS. + ''' +dependencies: + device_info: 2.0.3 + google_sign_in: 4.5.1 + android_alarm_manager: 0.4.5+11 + $dartPluginName: + path: ../$dartPluginName +''', ); await pubspec.writeAsString(content, flush: true); await inDirectory(projectDir, () async { @@ -191,7 +205,8 @@ Future main() async { || !podfileLockOutput.contains(':path: Flutter/FlutterPluginRegistrant') || !podfileLockOutput.contains(':path: ".symlinks/plugins/device_info/ios"') || !podfileLockOutput.contains(':path: ".symlinks/plugins/google_sign_in/ios"') - || podfileLockOutput.contains('android_alarm_manager')) { + || podfileLockOutput.contains('android_alarm_manager') + || podfileLockOutput.contains(dartPluginName)) { print(podfileLockOutput); return TaskResult.failure('Building ephemeral host app Podfile.lock does not contain expected pods'); } @@ -205,6 +220,9 @@ Future main() async { // Android-only, no embedded framework. checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', 'android_alarm_manager.framework')); + // Dart-only, no embedded framework. + checkDirectoryNotExists(path.join(ephemeralIOSHostApp.path, 'Frameworks', '$dartPluginName.framework')); + section('Clean and pub get module'); await inDirectory(projectDir, () async { @@ -243,7 +261,8 @@ Future main() async { || !hostPodfileLockOutput.contains(':path: "../hello/.ios/Flutter/FlutterPluginRegistrant"') || !hostPodfileLockOutput.contains(':path: "../hello/.ios/.symlinks/plugins/device_info/ios"') || !hostPodfileLockOutput.contains(':path: "../hello/.ios/.symlinks/plugins/google_sign_in/ios"') - || hostPodfileLockOutput.contains('android_alarm_manager')) { + || hostPodfileLockOutput.contains('android_alarm_manager') + || hostPodfileLockOutput.contains(dartPluginName)) { print(hostPodfileLockOutput); throw TaskResult.failure('Building host app Podfile.lock does not contain expected pods'); } @@ -501,3 +520,46 @@ Future _isAppAotBuild(Directory app) async { return symbolTable.contains('kDartIsolateSnapshotInstructions'); } + +Future _createFakeDartPlugin(String name, Directory parent) async { + // Start from a standard plugin template. + await inDirectory(parent, () async { + await flutter( + 'create', + options: [ + '--org', + 'io.flutter.devicelab', + '--template=plugin', + '--platforms=ios', + name, + ], + ); + }); + + final String pluginDir = path.join(parent.path, name); + + // Convert the metadata to Dart-only. + final String dartPluginClass = 'DartClassFor$name'; + final File pubspec = File(path.join(pluginDir, 'pubspec.yaml')); + String content = await pubspec.readAsString(); + content = content.replaceAll( + RegExp(r' pluginClass: .*?\n'), + ' dartPluginClass: $dartPluginClass\n', + ); + await pubspec.writeAsString(content, flush: true); + + // Add the Dart registration hook that the build will generate a call to. + final File dartCode = File(path.join(pluginDir, 'lib', '$name.dart')); + content = await dartCode.readAsString(); + content = ''' +$content + +class $dartPluginClass { + static void registerWith() {} +} +'''; + await dartCode.writeAsString(content, flush: true); + + // Remove the native plugin code. + await Directory(path.join(pluginDir, 'ios')).delete(recursive: true); +} diff --git a/dev/devicelab/bin/tasks/plugin_test.dart b/dev/devicelab/bin/tasks/plugin_test.dart index d7b5e536f5..9b556cded7 100644 --- a/dev/devicelab/bin/tasks/plugin_test.dart +++ b/dev/devicelab/bin/tasks/plugin_test.dart @@ -16,5 +16,7 @@ Future main() async { {'ENABLE_ANDROID_EMBEDDING_V2': 'true'}), PluginTest('apk', ['-a', 'kotlin', '--platforms=android'], pluginCreateEnvironment: {'ENABLE_ANDROID_EMBEDDING_V2': 'true'}), + // Test that Dart-only plugins are supported. + PluginTest('apk', ['--platforms=android'], dartOnlyPlugin: true), ])); } diff --git a/dev/devicelab/bin/tasks/plugin_test_ios.dart b/dev/devicelab/bin/tasks/plugin_test_ios.dart index bff25a2120..408cf1b87b 100644 --- a/dev/devicelab/bin/tasks/plugin_test_ios.dart +++ b/dev/devicelab/bin/tasks/plugin_test_ios.dart @@ -10,5 +10,8 @@ Future main() async { PluginTest('ios', ['-i', 'objc', '--platforms=ios']), PluginTest('ios', ['-i', 'swift', '--platforms=ios']), PluginTest('macos', ['--platforms=macos']), + // Test that Dart-only plugins are supported. + PluginTest('ios', ['--platforms=ios'], dartOnlyPlugin: true), + PluginTest('macos', ['--platforms=macos'], dartOnlyPlugin: true), ])); } diff --git a/dev/devicelab/lib/tasks/plugin_tests.dart b/dev/devicelab/lib/tasks/plugin_tests.dart index 05fc5510dd..4de88a284a 100644 --- a/dev/devicelab/lib/tasks/plugin_tests.dart +++ b/dev/devicelab/lib/tasks/plugin_tests.dart @@ -26,12 +26,19 @@ TaskFunction combine(List tasks) { /// Defines task that creates new Flutter project, adds a local and remote /// plugin, and then builds the specified [buildTarget]. class PluginTest { - PluginTest(this.buildTarget, this.options, { this.pluginCreateEnvironment, this.appCreateEnvironment }); + PluginTest( + this.buildTarget, + this.options, { + this.pluginCreateEnvironment, + this.appCreateEnvironment, + this.dartOnlyPlugin = false, + }); final String buildTarget; final List options; final Map? pluginCreateEnvironment; final Map? appCreateEnvironment; + final bool dartOnlyPlugin; Future call() async { final Directory tempDir = @@ -41,6 +48,9 @@ class PluginTest { final _FlutterProject plugin = await _FlutterProject.create( tempDir, options, buildTarget, name: 'plugintest', template: 'plugin', environment: pluginCreateEnvironment); + if (dartOnlyPlugin) { + await plugin.convertDefaultPluginToDartPlugin(); + } section('Test plugin'); await plugin.test(); section('Create Flutter app'); @@ -52,7 +62,7 @@ class PluginTest { pluginPath: path.join('..', 'plugintest')); await app.addPlugin('path_provider'); section('Build app'); - await app.build(buildTarget); + await app.build(buildTarget, validateNativeBuildProject: !dartOnlyPlugin); section('Test app'); await app.test(); } finally { @@ -76,8 +86,10 @@ class _FlutterProject { String get rootPath => path.join(parent.path, name); + File get pubspecFile => File(path.join(rootPath, 'pubspec.yaml')); + Future addPlugin(String plugin, {String? pluginPath}) async { - final File pubspec = File(path.join(rootPath, 'pubspec.yaml')); + final File pubspec = pubspecFile; String content = await pubspec.readAsString(); final String dependency = pluginPath != null ? '$plugin:\n path: $pluginPath' : '$plugin:'; @@ -88,6 +100,47 @@ class _FlutterProject { await pubspec.writeAsString(content, flush: true); } + /// Converts a plugin created from the standard template to a Dart-only + /// plugin. + Future convertDefaultPluginToDartPlugin() async { + final String dartPluginClass = 'DartClassFor$name'; + // Convert the metadata. + final File pubspec = pubspecFile; + String content = await pubspec.readAsString(); + content = content.replaceAll( + RegExp(r' pluginClass: .*?\n'), + ' dartPluginClass: $dartPluginClass\n', + ); + await pubspec.writeAsString(content, flush: true); + + // Add the Dart registration hook that the build will generate a call to. + final File dartCode = File(path.join(rootPath, 'lib', '$name.dart')); + content = await dartCode.readAsString(); + content = ''' +$content + +class $dartPluginClass { + static void registerWith() {} +} +'''; + await dartCode.writeAsString(content, flush: true); + + // Remove any native plugin code. + const List platforms = [ + 'android', + 'ios', + 'linux', + 'macos', + 'windows', + ]; + for (final String platform in platforms) { + final Directory platformDir = Directory(path.join(rootPath, platform)); + if (platformDir.existsSync()) { + await platformDir.delete(recursive: true); + } + } + } + Future test() async { await inDirectory(Directory(rootPath), () async { await flutter('test'); @@ -147,7 +200,7 @@ class _FlutterProject { podspec.writeAsStringSync(podspecContent, flush: true); } - Future build(String target) async { + Future build(String target, {bool validateNativeBuildProject = true}) async { await inDirectory(Directory(rootPath), () async { final String buildOutput = await evalFlutter('build', options: [ target, @@ -167,28 +220,30 @@ class _FlutterProject { throw TaskResult.failure('Minimum plugin version warning present'); } - final File podsProject = File(path.join(rootPath, target, 'Pods', 'Pods.xcodeproj', 'project.pbxproj')); - if (!podsProject.existsSync()) { - throw TaskResult.failure('Xcode Pods project file missing at ${podsProject.path}'); - } - - final String podsProjectContent = podsProject.readAsStringSync(); - if (target == 'ios') { - // Plugins with versions lower than the app version should not have IPHONEOS_DEPLOYMENT_TARGET set. - // The plugintest plugin target should not have IPHONEOS_DEPLOYMENT_TARGET set since it has been lowered - // in _reduceDarwinPluginMinimumVersion to 7, which is below the target version of 9. - if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 7')) { - throw TaskResult.failure('Plugin build setting IPHONEOS_DEPLOYMENT_TARGET not removed'); + if (validateNativeBuildProject) { + final File podsProject = File(path.join(rootPath, target, 'Pods', 'Pods.xcodeproj', 'project.pbxproj')); + if (!podsProject.existsSync()) { + throw TaskResult.failure('Xcode Pods project file missing at ${podsProject.path}'); } - if (!podsProjectContent.contains(r'"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "$(inherited) i386";')) { - throw TaskResult.failure(r'EXCLUDED_ARCHS is not "$(inherited) i386"'); - } - } - // Same for macOS deployment target, but 10.8. - // The plugintest target should not have MACOSX_DEPLOYMENT_TARGET set. - if (target == 'macos' && podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.8')) { - throw TaskResult.failure('Plugin build setting MACOSX_DEPLOYMENT_TARGET not removed'); + final String podsProjectContent = podsProject.readAsStringSync(); + if (target == 'ios') { + // Plugins with versions lower than the app version should not have IPHONEOS_DEPLOYMENT_TARGET set. + // The plugintest plugin target should not have IPHONEOS_DEPLOYMENT_TARGET set since it has been lowered + // in _reduceDarwinPluginMinimumVersion to 7, which is below the target version of 9. + if (podsProjectContent.contains('IPHONEOS_DEPLOYMENT_TARGET = 7')) { + throw TaskResult.failure('Plugin build setting IPHONEOS_DEPLOYMENT_TARGET not removed'); + } + if (!podsProjectContent.contains(r'"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "$(inherited) i386";')) { + throw TaskResult.failure(r'EXCLUDED_ARCHS is not "$(inherited) i386"'); + } + } + + // Same for macOS deployment target, but 10.8. + // The plugintest target should not have MACOSX_DEPLOYMENT_TARGET set. + if (target == 'macos' && podsProjectContent.contains('MACOSX_DEPLOYMENT_TARGET = 10.8')) { + throw TaskResult.failure('Plugin build setting MACOSX_DEPLOYMENT_TARGET not removed'); + } } } }); diff --git a/packages/flutter_tools/bin/podhelper.rb b/packages/flutter_tools/bin/podhelper.rb index 97e073dd84..7f11888644 100644 --- a/packages/flutter_tools/bin/podhelper.rb +++ b/packages/flutter_tools/bin/podhelper.rb @@ -255,7 +255,8 @@ def flutter_install_plugin_pods(application_path = nil, relative_symlink_dir, pl plugin_pods.each do |plugin_hash| plugin_name = plugin_hash['name'] plugin_path = plugin_hash['path'] - if (plugin_name && plugin_path) + has_native_build = plugin_hash.fetch('native_build', true) + if (plugin_name && plugin_path && has_native_build) symlink = File.join(symlink_plugins_dir, plugin_name) File.symlink(plugin_path, symlink) diff --git a/packages/flutter_tools/gradle/app_plugin_loader.gradle b/packages/flutter_tools/gradle/app_plugin_loader.gradle index f722ea8b5c..ed92e8ef7c 100644 --- a/packages/flutter_tools/gradle/app_plugin_loader.gradle +++ b/packages/flutter_tools/gradle/app_plugin_loader.gradle @@ -23,6 +23,12 @@ assert object.plugins.android instanceof List object.plugins.android.each { androidPlugin -> assert androidPlugin.name instanceof String assert androidPlugin.path instanceof String + // Skip plugins that have no native build (such as a Dart-only implementation + // of a federated plugin). + def needsBuild = androidPlugin.containsKey('native_build') ? androidPlugin['native_build'] : true + if (!needsBuild) { + return + } def pluginDirectory = new File(androidPlugin.path, 'android') assert pluginDirectory.exists() include ":${androidPlugin.name}" diff --git a/packages/flutter_tools/gradle/module_plugin_loader.gradle b/packages/flutter_tools/gradle/module_plugin_loader.gradle index ebce1091fc..d0b7287522 100644 --- a/packages/flutter_tools/gradle/module_plugin_loader.gradle +++ b/packages/flutter_tools/gradle/module_plugin_loader.gradle @@ -20,6 +20,12 @@ if (pluginsFile.exists()) { object.plugins.android.each { androidPlugin -> assert androidPlugin.name instanceof String assert androidPlugin.path instanceof String + // Skip plugins that have no native build (such as a Dart-only + // implementation of a federated plugin). + def needsBuild = androidPlugin.containsKey('native_build') ? androidPlugin['native_build'] : true + if (!needsBuild) { + return + } def pluginDirectory = new File(androidPlugin.path, 'android') assert pluginDirectory.exists() include ":${androidPlugin.name}" diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart index 65a845e7bf..2520df5e3e 100644 --- a/packages/flutter_tools/lib/src/flutter_plugins.dart +++ b/packages/flutter_tools/lib/src/flutter_plugins.dart @@ -98,6 +98,7 @@ const String _kFlutterPluginsPluginListKey = 'plugins'; const String _kFlutterPluginsNameKey = 'name'; const String _kFlutterPluginsPathKey = 'path'; const String _kFlutterPluginsDependenciesKey = 'dependencies'; +const String _kFlutterPluginsHasNativeBuildKey = 'native_build'; /// Filters [plugins] to those supported by [platformKey]. List> _filterPluginsByPlatform(List plugins, String platformKey) { @@ -108,9 +109,13 @@ List> _filterPluginsByPlatform(List plugins, String final Set pluginNames = platformPlugins.map((Plugin plugin) => plugin.name).toSet(); final List> pluginInfo = >[]; for (final Plugin plugin in platformPlugins) { + // This is guaranteed to be non-null due to the `where` filter above. + final PluginPlatform platformPlugin = plugin.platforms[platformKey]!; pluginInfo.add({ _kFlutterPluginsNameKey: plugin.name, _kFlutterPluginsPathKey: globals.fsUtils.escapePath(plugin.path), + if (platformPlugin is NativeOrDartPlugin) + _kFlutterPluginsHasNativeBuildKey: (platformPlugin as NativeOrDartPlugin).isNative(), _kFlutterPluginsDependenciesKey: [...plugin.dependencies.where(pluginNames.contains)], }); } @@ -130,7 +135,8 @@ List> _filterPluginsByPlatform(List plugins, String /// "dependencies": [ /// "plugin-a", /// "plugin-b" -/// ] +/// ], +/// "native_build": true /// } /// ], /// "android": [], diff --git a/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/podhelper.rb.tmpl b/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/podhelper.rb.tmpl index 2f3d6851d3..2dbaaaa94c 100644 --- a/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/podhelper.rb.tmpl +++ b/packages/flutter_tools/templates/module/ios/library/Flutter.tmpl/podhelper.rb.tmpl @@ -81,7 +81,8 @@ def install_flutter_plugin_pods(flutter_application_path) plugin_pods.each do |plugin_hash| plugin_name = plugin_hash['name'] plugin_path = plugin_hash['path'] - if (plugin_name && plugin_path) + has_native_build = plugin_hash.fetch('native_build', true) + if (plugin_name && plugin_path && has_native_build) symlink = File.join(symlinks_dir, plugin_name) FileUtils.rm_f(symlink) File.symlink(plugin_path, symlink) diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart index da384369c7..a51ffc89a3 100644 --- a/packages/flutter_tools/test/general.shard/plugins_test.dart +++ b/packages/flutter_tools/test/general.shard/plugins_test.dart @@ -30,6 +30,46 @@ import '../src/context.dart'; import '../src/fakes.dart' hide FakeOperatingSystemUtils; import '../src/pubspec_schema.dart'; +/// Information for a platform entry in the 'platforms' section of a plugin's +/// pubspec.yaml. +class _PluginPlatformInfo { + const _PluginPlatformInfo({ + this.pluginClass, + this.dartPluginClass, + this.androidPackage, + this.fileName + }) : assert(pluginClass != null || dartPluginClass != null), + assert(androidPackage == null || pluginClass != null); + + /// The pluginClass entry, if any. + final String pluginClass; + + /// The dartPluginClass entry, if any. + final String dartPluginClass; + + /// The package entry for an Android plugin implementation using pluginClass. + final String androidPackage; + + /// The fileName entry for a web plugin implementation. + final String fileName; + + /// Returns the body of a platform section for a plugin's pubspec, properly + /// indented. + String get indentedPubspecSection { + const String indentation = ' '; + return [ + if (pluginClass != null) + '${indentation}pluginClass: $pluginClass', + if (dartPluginClass != null) + '${indentation}dartPluginClass: $dartPluginClass', + if (androidPackage != null) + '${indentation}package: $androidPackage', + if (fileName != null) + '${indentation}fileName: $fileName', + ].join('\n'); + } +} + void main() { group('plugins', () { FileSystem fs; @@ -331,7 +371,7 @@ flutter: ); } - Directory createPluginWithDependencies({ + Directory createLegacyPluginWithDependencies({ @required String name, @required List dependencies, }) { @@ -347,6 +387,44 @@ flutter: plugin: androidPackage: plugin2 pluginClass: UseNewEmbedding +dependencies: +'''); + for (final String dependency in dependencies) { + pluginDirectory + .childFile('pubspec.yaml') + .writeAsStringSync(' $dependency:\n', mode: FileMode.append); + } + flutterProject.directory + .childFile('.packages') + .writeAsStringSync( + '$name:${pluginDirectory.childDirectory('lib').uri.toString()}\n', + mode: FileMode.append, + ); + return pluginDirectory; + } + + Directory createPlugin({ + @required String name, + @required Map platforms, + List dependencies = const [], + }) { + assert(name != null); + assert(dependencies != null); + + final Iterable platformSections = platforms.entries.map((MapEntry entry) => ''' + ${entry.key}: +${entry.value.indentedPubspecSection} +'''); + final Directory pluginDirectory = fs.systemTempDirectory.createTempSync('flutter_plugin.'); + pluginDirectory + .childFile('pubspec.yaml') + .writeAsStringSync(''' +name: $name +flutter: + plugin: + platforms: +${platformSections.join('\n')} + dependencies: '''); for (final String dependency in dependencies) { @@ -420,9 +498,9 @@ dependencies: testUsingContext( 'Refreshing the plugin list modifies .flutter-plugins ' 'and .flutter-plugins-dependencies when there are plugins', () async { - final Directory pluginA = createPluginWithDependencies(name: 'plugin-a', dependencies: const ['plugin-b', 'plugin-c', 'random-package']); - final Directory pluginB = createPluginWithDependencies(name: 'plugin-b', dependencies: const ['plugin-c']); - final Directory pluginC = createPluginWithDependencies(name: 'plugin-c', dependencies: const []); + final Directory pluginA = createLegacyPluginWithDependencies(name: 'plugin-a', dependencies: const ['plugin-b', 'plugin-c', 'random-package']); + final Directory pluginB = createLegacyPluginWithDependencies(name: 'plugin-b', dependencies: const ['plugin-c']); + final Directory pluginC = createLegacyPluginWithDependencies(name: 'plugin-c', dependencies: const []); iosProject.testExists = true; final DateTime dateCreated = DateTime(1970); @@ -449,22 +527,25 @@ dependencies: { 'name': 'plugin-a', 'path': '${pluginA.path}/', + 'native_build': true, 'dependencies': [ 'plugin-b', 'plugin-c' - ] + ], }, { 'name': 'plugin-b', 'path': '${pluginB.path}/', + 'native_build': true, 'dependencies': [ 'plugin-c' - ] + ], }, { 'name': 'plugin-c', 'path': '${pluginC.path}/', - 'dependencies': [] + 'native_build': true, + 'dependencies': [], }, ]; expect(plugins['ios'], expectedPlugins); @@ -514,6 +595,51 @@ dependencies: FlutterVersion: () => flutterVersion }); + testUsingContext( + '.flutter-plugins-dependencies indicates native build inclusion', () async { + createPlugin( + name: 'plugin-a', + platforms: const { + // Native-only; should include native build. + 'android': _PluginPlatformInfo(pluginClass: 'Foo', androidPackage: 'bar.foo'), + // Hybrid native and Dart; should include native build. + 'ios': _PluginPlatformInfo(pluginClass: 'Foo', dartPluginClass: 'Bar'), + // Web; should not have the native build key at all since it doesn't apply. + 'web': _PluginPlatformInfo(pluginClass: 'Foo', fileName: 'lib/foo.dart'), + // Dart-only; should not include native build. + 'windows': _PluginPlatformInfo(dartPluginClass: 'Foo'), + }); + iosProject.testExists = true; + + final DateTime dateCreated = DateTime(1970); + systemClock.currentTime = dateCreated; + + await refreshPluginsList(flutterProject); + + expect(flutterProject.flutterPluginsDependenciesFile.existsSync(), true); + final String pluginsString = flutterProject.flutterPluginsDependenciesFile.readAsStringSync(); + final Map jsonContent = json.decode(pluginsString) as Map; + final Map plugins = jsonContent['plugins'] as Map; + + // Extracts the native_build key (if any) from the first plugin for the + // given platform. + bool getNativeBuildValue(String platform) { + final List> platformPlugins = (plugins[platform] + as List).cast>(); + expect(platformPlugins.length, 1); + return platformPlugins[0]['native_build'] as bool; + } + expect(getNativeBuildValue('android'), true); + expect(getNativeBuildValue('ios'), true); + expect(getNativeBuildValue('web'), null); + expect(getNativeBuildValue('windows'), false); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + SystemClock: () => systemClock, + FlutterVersion: () => flutterVersion + }); + testUsingContext('Changes to the plugin list invalidates the Cocoapod lockfiles', () async { simulatePodInstallRun(iosProject); simulatePodInstallRun(macosProject);