Implement dartPluginClass support for plugins (#74469)

diff --git a/dev/devicelab/bin/tasks/dart_plugin_registry_test.dart b/dev/devicelab/bin/tasks/dart_plugin_registry_test.dart
new file mode 100644
index 0000000..50c50ae
--- /dev/null
+++ b/dev/devicelab/bin/tasks/dart_plugin_registry_test.dart
@@ -0,0 +1,10 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_devicelab/tasks/dart_plugin_registry_tests.dart';
+import 'package:flutter_devicelab/framework/framework.dart';
+
+Future<void> main() async {
+  await task(dartPluginRegistryTest());
+}
diff --git a/dev/devicelab/lib/tasks/dart_plugin_registry_tests.dart b/dev/devicelab/lib/tasks/dart_plugin_registry_tests.dart
new file mode 100644
index 0000000..79fcb1e
--- /dev/null
+++ b/dev/devicelab/lib/tasks/dart_plugin_registry_tests.dart
@@ -0,0 +1,181 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+import 'package:flutter_devicelab/framework/framework.dart';
+import 'package:flutter_devicelab/framework/task_result.dart';
+import 'package:flutter_devicelab/framework/utils.dart';
+
+TaskFunction dartPluginRegistryTest({
+  String deviceIdOverride,
+  Map<String, String> environment,
+}) {
+  final Directory tempDir = Directory.systemTemp
+      .createTempSync('flutter_devicelab_dart_plugin_test.');
+  return () async {
+    try {
+      section('Create implementation plugin');
+      await inDirectory(tempDir, () async {
+        await flutter(
+          'create',
+          options: <String>[
+            '--template=plugin',
+            '--org',
+            'io.flutter.devicelab',
+            '--platforms',
+            'macos',
+            'plugin_platform_implementation',
+          ],
+          environment: environment,
+        );
+      });
+
+      final File pluginMain = File(path.join(
+        tempDir.absolute.path,
+        'plugin_platform_implementation',
+        'lib',
+        'plugin_platform_implementation.dart',
+      ));
+      if (!pluginMain.existsSync()) {
+        return TaskResult.failure('${pluginMain.path} does not exist');
+      }
+
+      // Patch plugin main dart file.
+      await pluginMain.writeAsString('''
+class PluginPlatformInterfaceMacOS {
+  static void registerWith() {
+    print('PluginPlatformInterfaceMacOS.registerWith() was called');
+  }
+}
+''', flush: true);
+
+      // Patch plugin main pubspec file.
+      final File pluginImplPubspec = File(path.join(
+        tempDir.absolute.path,
+        'plugin_platform_implementation',
+        'pubspec.yaml',
+      ));
+      String pluginImplPubspecContent = await pluginImplPubspec.readAsString();
+      pluginImplPubspecContent = pluginImplPubspecContent.replaceFirst(
+        '        pluginClass: PluginPlatformImplementationPlugin',
+        '        pluginClass: PluginPlatformImplementationPlugin\n'
+            '        dartPluginClass: PluginPlatformInterfaceMacOS\n',
+      );
+      pluginImplPubspecContent = pluginImplPubspecContent.replaceFirst(
+          '    platforms:\n',
+          '    implements: plugin_platform_interface\n'
+              '    platforms:\n');
+      await pluginImplPubspec.writeAsString(pluginImplPubspecContent,
+          flush: true);
+
+      section('Create interface plugin');
+      await inDirectory(tempDir, () async {
+        await flutter(
+          'create',
+          options: <String>[
+            '--template=plugin',
+            '--org',
+            'io.flutter.devicelab',
+            '--platforms',
+            'macos',
+            'plugin_platform_interface',
+          ],
+          environment: environment,
+        );
+      });
+      final File pluginInterfacePubspec = File(path.join(
+        tempDir.absolute.path,
+        'plugin_platform_interface',
+        'pubspec.yaml',
+      ));
+      String pluginInterfacePubspecContent =
+          await pluginInterfacePubspec.readAsString();
+      pluginInterfacePubspecContent =
+          pluginInterfacePubspecContent.replaceFirst(
+              '        pluginClass: PluginPlatformInterfacePlugin',
+              '        default_package: plugin_platform_implementation\n');
+      pluginInterfacePubspecContent =
+          pluginInterfacePubspecContent.replaceFirst(
+              'dependencies:',
+              'dependencies:\n'
+                  '  plugin_platform_implementation:\n'
+                  '    path: ../plugin_platform_implementation\n');
+      await pluginInterfacePubspec.writeAsString(pluginInterfacePubspecContent,
+          flush: true);
+
+      section('Create app');
+
+      await inDirectory(tempDir, () async {
+        await flutter(
+          'create',
+          options: <String>[
+            '--template=app',
+            '--org',
+            'io.flutter.devicelab',
+            '--platforms',
+            'macos',
+            'app',
+          ],
+          environment: environment,
+        );
+      });
+
+      final File appPubspec = File(path.join(
+        tempDir.absolute.path,
+        'app',
+        'pubspec.yaml',
+      ));
+      String appPubspecContent = await appPubspec.readAsString();
+      appPubspecContent = appPubspecContent.replaceFirst(
+          'dependencies:',
+          'dependencies:\n'
+              '  plugin_platform_interface:\n'
+              '    path: ../plugin_platform_interface\n');
+      await appPubspec.writeAsString(appPubspecContent, flush: true);
+
+      section('Flutter run for macos');
+
+      await inDirectory(path.join(tempDir.path, 'app'), () async {
+        final Process run = await startProcess(
+          path.join(flutterDirectory.path, 'bin', 'flutter'),
+          flutterCommandArgs('run', <String>['-d', 'macos', '-v']),
+          environment: null,
+        );
+        Completer<void> registryExecutedCompleter = Completer<void>();
+        final StreamSubscription<void> subscription = run.stdout
+            .transform<String>(utf8.decoder)
+            .transform<String>(const LineSplitter())
+            .listen((String line) {
+          if (line.contains(
+              'PluginPlatformInterfaceMacOS.registerWith() was called')) {
+            registryExecutedCompleter.complete();
+          }
+          print('stdout: $line');
+        });
+
+        section('Wait for registry execution');
+        await registryExecutedCompleter.future
+            .timeout(const Duration(minutes: 1));
+
+        // Hot restart.
+        run.stdin.write('R');
+        registryExecutedCompleter = Completer<void>();
+
+        section('Wait for registry execution after hot restart');
+        await registryExecutedCompleter.future
+            .timeout(const Duration(minutes: 1));
+
+        subscription.cancel();
+        run.kill();
+      });
+      return TaskResult.success(null);
+    } finally {
+      rmTree(tempDir);
+    }
+  };
+}
diff --git a/packages/flutter_tools/lib/src/build_system/build_system.dart b/packages/flutter_tools/lib/src/build_system/build_system.dart
index 48013e8..9abddf1 100644
--- a/packages/flutter_tools/lib/src/build_system/build_system.dart
+++ b/packages/flutter_tools/lib/src/build_system/build_system.dart
@@ -308,6 +308,7 @@
     @required Artifacts artifacts,
     @required ProcessManager processManager,
     @required String engineVersion,
+    @required bool generateDartPluginRegistry,
     Directory buildDir,
     Map<String, String> defines = const <String, String>{},
     Map<String, String> inputs = const <String, String>{},
@@ -347,6 +348,7 @@
       processManager: processManager,
       engineVersion: engineVersion,
       inputs: inputs,
+      generateDartPluginRegistry: generateDartPluginRegistry,
     );
   }
 
@@ -363,6 +365,7 @@
     Map<String, String> defines = const <String, String>{},
     Map<String, String> inputs = const <String, String>{},
     String engineVersion,
+    bool generateDartPluginRegistry = false,
     @required FileSystem fileSystem,
     @required Logger logger,
     @required Artifacts artifacts,
@@ -381,6 +384,7 @@
       artifacts: artifacts,
       processManager: processManager,
       engineVersion: engineVersion,
+      generateDartPluginRegistry: generateDartPluginRegistry,
     );
   }
 
@@ -398,6 +402,7 @@
     @required this.artifacts,
     @required this.engineVersion,
     @required this.inputs,
+    @required this.generateDartPluginRegistry,
   });
 
   /// The [Source] value which is substituted with the path to [projectDir].
@@ -475,6 +480,11 @@
 
   /// The version of the current engine, or `null` if built with a local engine.
   final String engineVersion;
+
+  /// Whether to generate the Dart plugin registry.
+  /// When [true], the main entrypoint is wrapped and the wrapper becomes
+  /// the new entrypoint.
+  final bool generateDartPluginRegistry;
 }
 
 /// The result information from the build system.
diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart
index cf25631..7cc6f96 100644
--- a/packages/flutter_tools/lib/src/build_system/targets/common.dart
+++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart
@@ -279,6 +279,8 @@
       fileSystemScheme: fileSystemScheme,
       dartDefines: decodeDartDefines(environment.defines, kDartDefines),
       packageConfig: packageConfig,
+      buildDir: environment.buildDir,
+      generateDartPluginRegistry: environment.generateDartPluginRegistry,
     );
     if (output == null || output.errorCount != 0) {
       throw Exception();
diff --git a/packages/flutter_tools/lib/src/bundle.dart b/packages/flutter_tools/lib/src/bundle.dart
index 8263c32..f89d00b 100644
--- a/packages/flutter_tools/lib/src/bundle.dart
+++ b/packages/flutter_tools/lib/src/bundle.dart
@@ -160,6 +160,7 @@
     fileSystem: globals.fs,
     logger: globals.logger,
     processManager: globals.processManager,
+    generateDartPluginRegistry: true,
   );
   final Target target = buildMode == BuildMode.debug
     ? const CopyFlutterBundle()
diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart
index d7e72d7..322b860 100644
--- a/packages/flutter_tools/lib/src/commands/assemble.dart
+++ b/packages/flutter_tools/lib/src/commands/assemble.dart
@@ -195,7 +195,8 @@
       processManager: globals.processManager,
       engineVersion: globals.artifacts.isLocalEngine
         ? null
-        : globals.flutterVersion.engineRevision
+        : globals.flutterVersion.engineRevision,
+      generateDartPluginRegistry: true,
     );
     return result;
   }
diff --git a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
index aed1d63..b78381f 100644
--- a/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
+++ b/packages/flutter_tools/lib/src/commands/build_ios_framework.dart
@@ -385,6 +385,7 @@
           engineVersion: globals.artifacts.isLocalEngine
               ? null
               : globals.flutterVersion.engineRevision,
+          generateDartPluginRegistry: true,
         );
         Target target;
         // Always build debug for simulator.
diff --git a/packages/flutter_tools/lib/src/commands/packages.dart b/packages/flutter_tools/lib/src/commands/packages.dart
index 606ae98..aa40ccf 100644
--- a/packages/flutter_tools/lib/src/commands/packages.dart
+++ b/packages/flutter_tools/lib/src/commands/packages.dart
@@ -119,6 +119,7 @@
         outputDir: globals.fs.directory(getBuildDirectory()),
         processManager: globals.processManager,
         projectDir: flutterProject.directory,
+        generateDartPluginRegistry: true,
       );
 
       await generateLocalizationsSyntheticPackage(
@@ -324,6 +325,7 @@
         outputDir: globals.fs.directory(getBuildDirectory()),
         processManager: globals.processManager,
         projectDir: flutterProject.directory,
+        generateDartPluginRegistry: true,
       );
 
       await generateLocalizationsSyntheticPackage(
diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart
index 76923c2..1f9f584 100644
--- a/packages/flutter_tools/lib/src/compile.dart
+++ b/packages/flutter_tools/lib/src/compile.dart
@@ -19,6 +19,8 @@
 import 'base/platform.dart';
 import 'build_info.dart';
 import 'convert.dart';
+import 'plugins.dart';
+import 'project.dart';
 
 /// The target model describes the set of core libraries that are available within
 /// the SDK.
@@ -209,6 +211,8 @@
     String fileSystemScheme,
     String initializeFromDill,
     String platformDill,
+    Directory buildDir,
+    bool generateDartPluginRegistry = false,
     @required String packagesPath,
     @required BuildMode buildMode,
     @required bool trackWidgetCreation,
@@ -227,7 +231,8 @@
       throwToolExit('Unable to find Dart binary at $engineDartPath');
     }
     String mainUri;
-    final Uri mainFileUri = _fileSystem.file(mainPath).uri;
+    final File mainFile = _fileSystem.file(mainPath);
+    final Uri mainFileUri = mainFile.uri;
     if (packagesPath != null) {
       mainUri = packageConfig.toPackageUri(mainFileUri)?.toString();
     }
@@ -235,6 +240,21 @@
     if (outputFilePath != null && !_fileSystem.isFileSync(outputFilePath)) {
       _fileSystem.file(outputFilePath).createSync(recursive: true);
     }
+    if (buildDir != null && generateDartPluginRegistry) {
+      // `generated_main.dart` is under `.dart_tools/flutter_build/`,
+      // so the resident compiler can find it.
+      final File newMainDart = buildDir.parent.childFile('generated_main.dart');
+      if (await generateMainDartWithPluginRegistrant(
+        FlutterProject.current(),
+        packageConfig,
+        mainUri,
+        newMainDart,
+        mainFile,
+      )) {
+        mainUri = newMainDart.path;
+      }
+    }
+
     final List<String> command = <String>[
       engineDartPath,
       '--disable-dart-dev',
@@ -579,7 +599,6 @@
     if (!_controller.hasListener) {
       _controller.stream.listen(_handleCompilationRequest);
     }
-
     final Completer<CompilerOutput> completer = Completer<CompilerOutput>();
     _controller.add(
       _RecompileRequest(completer, mainUri, invalidatedFiles, outputPath, packageConfig, suppressErrors)
diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart
index 6efb7de..ef16b4f 100644
--- a/packages/flutter_tools/lib/src/devfs.dart
+++ b/packages/flutter_tools/lib/src/devfs.dart
@@ -518,6 +518,20 @@
     // dill files that depend on the invalidated files.
     _logger.printTrace('Compiling dart to kernel with ${invalidatedFiles.length} updated files');
 
+    // `generated_main.dart` contains the Dart plugin registry.
+    if (projectRootPath != null) {
+      final File generatedMainDart = _fileSystem.file(
+        _fileSystem.path.join(
+          projectRootPath,
+          '.dart_tool',
+          'flutter_build',
+          'generated_main.dart',
+        ),
+      );
+      if (generatedMainDart != null && generatedMainDart.existsSync()) {
+        mainUri = generatedMainDart.uri;
+      }
+    }
     // Await the compiler response after checking if the bundle is updated. This allows the file
     // stating to be done while waiting for the frontend_server response.
     final Future<CompilerOutput> pendingCompilerOutput = generator.recompile(
diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart
index fa30fe7..05c8c07 100644
--- a/packages/flutter_tools/lib/src/flutter_manifest.dart
+++ b/packages/flutter_tools/lib/src/flutter_manifest.dart
@@ -86,6 +86,13 @@
   /// The string value of the top-level `name` property in the `pubspec.yaml` file.
   String get appName => _descriptor['name'] as String ?? '';
 
+  /// Contains the name of the dependencies.
+  /// These are the keys specified in the `dependency` map.
+  Set<String> get dependencies {
+    final YamlMap dependencies = _descriptor['dependencies'] as YamlMap;
+    return dependencies != null ? <String>{...dependencies.keys.cast<String>()} : <String>{};
+  }
+
   // Flag to avoid printing multiple invalid version messages.
   bool _hasShowInvalidVersionMsg = false;
 
diff --git a/packages/flutter_tools/lib/src/platform_plugins.dart b/packages/flutter_tools/lib/src/platform_plugins.dart
index d41fa74..452a626 100644
--- a/packages/flutter_tools/lib/src/platform_plugins.dart
+++ b/packages/flutter_tools/lib/src/platform_plugins.dart
@@ -16,6 +16,9 @@
 /// Constant for 'pluginClass' key in plugin maps.
 const String kDartPluginClass = 'dartPluginClass';
 
+// Constant for 'defaultPackage' key in plugin maps.
+const String kDefaultPackage = 'default_package';
+
 /// Marker interface for all platform specific plugin config implementations.
 abstract class PluginPlatform {
   const PluginPlatform();
@@ -207,6 +210,7 @@
     @required this.name,
     this.pluginClass,
     this.dartPluginClass,
+    this.defaultPackage,
   });
 
   factory MacOSPlugin.fromYaml(String name, YamlMap yaml) {
@@ -220,6 +224,7 @@
       name: name,
       pluginClass: pluginClass,
       dartPluginClass: yaml[kDartPluginClass] as String,
+      defaultPackage: yaml[kDefaultPackage] as String,
     );
   }
 
@@ -227,7 +232,9 @@
     if (yaml == null) {
       return false;
     }
-    return yaml[kPluginClass] is String || yaml[kDartPluginClass] is String;
+    return yaml[kPluginClass] is String ||
+           yaml[kDartPluginClass] is String ||
+           yaml[kDefaultPackage] is String;
   }
 
   static const String kConfigKey = 'macos';
@@ -235,6 +242,7 @@
   final String name;
   final String pluginClass;
   final String dartPluginClass;
+  final String defaultPackage;
 
   @override
   bool isNative() => pluginClass != null;
@@ -244,7 +252,8 @@
     return <String, dynamic>{
       'name': name,
       if (pluginClass != null) 'class': pluginClass,
-      if (dartPluginClass != null) 'dartPluginClass': dartPluginClass,
+      if (dartPluginClass != null) kDartPluginClass : dartPluginClass,
+      if (defaultPackage != null) kDefaultPackage : defaultPackage,
     };
   }
 }
@@ -258,7 +267,8 @@
     @required this.name,
     this.pluginClass,
     this.dartPluginClass,
-  }) : assert(pluginClass != null || dartPluginClass != null);
+    this.defaultPackage,
+  }) : assert(pluginClass != null || dartPluginClass != null || defaultPackage != null);
 
   factory WindowsPlugin.fromYaml(String name, YamlMap yaml) {
     assert(validate(yaml));
@@ -271,6 +281,7 @@
       name: name,
       pluginClass: pluginClass,
       dartPluginClass: yaml[kDartPluginClass] as String,
+      defaultPackage: yaml[kDefaultPackage] as String,
     );
   }
 
@@ -278,7 +289,9 @@
     if (yaml == null) {
       return false;
     }
-    return yaml[kDartPluginClass] is String || yaml[kPluginClass] is String;
+    return yaml[kPluginClass] is String ||
+           yaml[kDartPluginClass] is String ||
+           yaml[kDefaultPackage] is String;
   }
 
   static const String kConfigKey = 'windows';
@@ -286,6 +299,7 @@
   final String name;
   final String pluginClass;
   final String dartPluginClass;
+  final String defaultPackage;
 
   @override
   bool isNative() => pluginClass != null;
@@ -296,7 +310,8 @@
       'name': name,
       if (pluginClass != null) 'class': pluginClass,
       if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass),
-      if (dartPluginClass != null) 'dartPluginClass': dartPluginClass,
+      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
+      if (defaultPackage != null) kDefaultPackage: defaultPackage,
     };
   }
 }
@@ -310,7 +325,8 @@
     @required this.name,
     this.pluginClass,
     this.dartPluginClass,
-  }) : assert(pluginClass != null || dartPluginClass != null);
+    this.defaultPackage,
+  }) : assert(pluginClass != null || dartPluginClass != null || defaultPackage != null);
 
   factory LinuxPlugin.fromYaml(String name, YamlMap yaml) {
     assert(validate(yaml));
@@ -323,6 +339,7 @@
       name: name,
       pluginClass: pluginClass,
       dartPluginClass: yaml[kDartPluginClass] as String,
+      defaultPackage: yaml[kDefaultPackage] as String,
     );
   }
 
@@ -330,7 +347,9 @@
     if (yaml == null) {
       return false;
     }
-    return yaml[kPluginClass] is String || yaml[kDartPluginClass] is String;
+    return yaml[kPluginClass] is String ||
+           yaml[kDartPluginClass] is String ||
+           yaml[kDefaultPackage] is String;
   }
 
   static const String kConfigKey = 'linux';
@@ -338,6 +357,7 @@
   final String name;
   final String pluginClass;
   final String dartPluginClass;
+  final String defaultPackage;
 
   @override
   bool isNative() => pluginClass != null;
@@ -348,7 +368,8 @@
       'name': name,
       if (pluginClass != null) 'class': pluginClass,
       if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass),
-      if (dartPluginClass != null) 'dartPluginClass': dartPluginClass,
+      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
+      if (defaultPackage != null) kDefaultPackage: defaultPackage,
     };
   }
 }
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index 42c31fc..81751a5 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -17,6 +17,7 @@
 import 'base/platform.dart';
 import 'base/version.dart';
 import 'convert.dart';
+import 'dart/language_version.dart';
 import 'dart/package_map.dart';
 import 'features.dart';
 import 'globals.dart' as globals;
@@ -36,11 +37,18 @@
     @required this.name,
     @required this.path,
     @required this.platforms,
+    @required this.defaultPackagePlatforms,
+    @required this.pluginDartClassPlatforms,
     @required this.dependencies,
+    @required this.isDirectDependency,
+    this.implementsPackage,
   }) : assert(name != null),
        assert(path != null),
        assert(platforms != null),
-       assert(dependencies != null);
+       assert(defaultPackagePlatforms != null),
+       assert(pluginDartClassPlatforms != null),
+       assert(dependencies != null),
+       assert(isDirectDependency != null);
 
   /// Parses [Plugin] specification from the provided pluginYaml.
   ///
@@ -76,15 +84,30 @@
     YamlMap pluginYaml,
     List<String> dependencies, {
     @required FileSystem fileSystem,
+    Set<String> appDependencies,
   }) {
     final List<String> errors = validatePluginYaml(pluginYaml);
     if (errors.isNotEmpty) {
       throwToolExit('Invalid plugin specification $name.\n${errors.join('\n')}');
     }
     if (pluginYaml != null && pluginYaml['platforms'] != null) {
-      return Plugin._fromMultiPlatformYaml(name, path, pluginYaml, dependencies, fileSystem);
+      return Plugin._fromMultiPlatformYaml(
+        name,
+        path,
+        pluginYaml,
+        dependencies,
+        fileSystem,
+        appDependencies != null && appDependencies.contains(name),
+      );
     }
-    return Plugin._fromLegacyYaml(name, path, pluginYaml, dependencies, fileSystem);
+    return Plugin._fromLegacyYaml(
+      name,
+      path,
+      pluginYaml,
+      dependencies,
+      fileSystem,
+      appDependencies != null && appDependencies.contains(name),
+    );
   }
 
   factory Plugin._fromMultiPlatformYaml(
@@ -93,6 +116,7 @@
     dynamic pluginYaml,
     List<String> dependencies,
     FileSystem fileSystem,
+    bool isDirectDependency,
   ) {
     assert (pluginYaml != null && pluginYaml['platforms'] != null,
             'Invalid multi-platform plugin specification $name.');
@@ -137,11 +161,47 @@
           WindowsPlugin.fromYaml(name, platformsYaml[WindowsPlugin.kConfigKey] as YamlMap);
     }
 
+    final String defaultPackageForLinux =
+        _getDefaultPackageForPlatform(platformsYaml, LinuxPlugin.kConfigKey);
+
+    final String defaultPackageForMacOS =
+        _getDefaultPackageForPlatform(platformsYaml, MacOSPlugin.kConfigKey);
+
+    final String defaultPackageForWindows =
+        _getDefaultPackageForPlatform(platformsYaml, WindowsPlugin.kConfigKey);
+
+    final String defaultPluginDartClassForLinux =
+        _getPluginDartClassForPlatform(platformsYaml, LinuxPlugin.kConfigKey);
+
+    final String defaultPluginDartClassForMacOS =
+        _getPluginDartClassForPlatform(platformsYaml, MacOSPlugin.kConfigKey);
+
+    final String defaultPluginDartClassForWindows =
+        _getPluginDartClassForPlatform(platformsYaml, WindowsPlugin.kConfigKey);
+
     return Plugin(
       name: name,
       path: path,
       platforms: platforms,
+      defaultPackagePlatforms: <String, String>{
+        if (defaultPackageForLinux != null)
+          LinuxPlugin.kConfigKey : defaultPackageForLinux,
+        if (defaultPackageForMacOS != null)
+          MacOSPlugin.kConfigKey : defaultPackageForMacOS,
+        if (defaultPackageForWindows != null)
+          WindowsPlugin.kConfigKey : defaultPackageForWindows,
+      },
+      pluginDartClassPlatforms: <String, String>{
+        if (defaultPluginDartClassForLinux != null)
+          LinuxPlugin.kConfigKey : defaultPluginDartClassForLinux,
+        if (defaultPluginDartClassForMacOS != null)
+          MacOSPlugin.kConfigKey : defaultPluginDartClassForMacOS,
+        if (defaultPluginDartClassForWindows != null)
+          WindowsPlugin.kConfigKey : defaultPluginDartClassForWindows,
+      },
       dependencies: dependencies,
+      isDirectDependency: isDirectDependency,
+      implementsPackage: pluginYaml['implements'] != null ? pluginYaml['implements'] as String : '',
     );
   }
 
@@ -151,6 +211,7 @@
     dynamic pluginYaml,
     List<String> dependencies,
     FileSystem fileSystem,
+    bool isDirectDependency,
   ) {
     final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
     final String pluginClass = pluginYaml['pluginClass'] as String;
@@ -178,7 +239,10 @@
       name: name,
       path: path,
       platforms: platforms,
+      defaultPackagePlatforms: <String, String>{},
+      pluginDartClassPlatforms: <String, String>{},
       dependencies: dependencies,
+      isDirectDependency: isDirectDependency,
     );
   }
 
@@ -295,11 +359,41 @@
     return errors;
   }
 
-  static bool _providesImplementationForPlatform(YamlMap platformsYaml, String platformKey) {
+  static bool _supportsPlatform(YamlMap platformsYaml, String platformKey) {
     if (!platformsYaml.containsKey(platformKey)) {
       return false;
     }
-    if ((platformsYaml[platformKey] as YamlMap).containsKey('default_package')) {
+    if (platformsYaml[platformKey] is YamlMap) {
+      return true;
+    }
+    return false;
+  }
+
+  static String _getDefaultPackageForPlatform(YamlMap platformsYaml, String platformKey) {
+    if (!_supportsPlatform(platformsYaml, platformKey)) {
+      return null;
+    }
+    if ((platformsYaml[platformKey] as YamlMap).containsKey(kDefaultPackage)) {
+      return (platformsYaml[platformKey] as YamlMap)[kDefaultPackage] as String;
+    }
+    return null;
+  }
+
+  static String _getPluginDartClassForPlatform(YamlMap platformsYaml, String platformKey) {
+    if (!_supportsPlatform(platformsYaml, platformKey)) {
+      return null;
+    }
+    if ((platformsYaml[platformKey] as YamlMap).containsKey(kDartPluginClass)) {
+      return (platformsYaml[platformKey] as YamlMap)[kDartPluginClass] as String;
+    }
+    return null;
+  }
+
+  static bool _providesImplementationForPlatform(YamlMap platformsYaml, String platformKey) {
+    if (!_supportsPlatform(platformsYaml, platformKey)) {
+      return false;
+    }
+    if ((platformsYaml[platformKey] as YamlMap).containsKey(kDefaultPackage)) {
       return false;
     }
     return true;
@@ -308,14 +402,28 @@
   final String name;
   final String path;
 
+  /// The name of the interface package that this plugin implements.
+  /// If [null], this plugin doesn't implement an interface.
+  final String implementsPackage;
+
   /// The name of the packages this plugin depends on.
   final List<String> dependencies;
 
   /// This is a mapping from platform config key to the plugin platform spec.
   final Map<String, PluginPlatform> platforms;
+
+  /// This is a mapping from platform config key to the default package implementation.
+  final Map<String, String> defaultPackagePlatforms;
+
+  /// This is a mapping from platform config key to the plugin class for the given platform.
+  final Map<String, String> pluginDartClassPlatforms;
+
+  /// Whether this plugin is a direct dependency of the app.
+  /// If [false], the plugin is a dependency of another plugin.
+  final bool isDirectDependency;
 }
 
-Plugin _pluginFromPackage(String name, Uri packageRoot) {
+Plugin _pluginFromPackage(String name, Uri packageRoot, Set<String> appDependencies) {
   final String pubspecPath = globals.fs.path.fromUri(packageRoot.resolve('pubspec.yaml'));
   if (!globals.fs.isFileSync(pubspecPath)) {
     return null;
@@ -344,6 +452,7 @@
     flutterConfig['plugin'] as YamlMap,
     dependencies == null ? <String>[] : <String>[...dependencies.keys.cast<String>()],
     fileSystem: globals.fs,
+    appDependencies: appDependencies,
   );
 }
 
@@ -360,7 +469,11 @@
   );
   for (final Package package in packageConfig.packages) {
     final Uri packageRoot = package.packageUriRoot.resolve('..');
-    final Plugin plugin = _pluginFromPackage(package.name, packageRoot);
+    final Plugin plugin = _pluginFromPackage(
+      package.name,
+      packageRoot,
+      project.manifest.dependencies,
+    );
     if (plugin != null) {
       plugins.add(plugin);
     }
@@ -368,6 +481,130 @@
   return plugins;
 }
 
+/// Metadata associated with the resolution of a platform interface of a plugin.
+class PluginInterfaceResolution {
+  PluginInterfaceResolution({
+    @required this.plugin,
+    this.platform,
+  }) : assert(plugin != null);
+
+  /// The plugin.
+  final Plugin plugin;
+  // The name of the platform that this plugin implements.
+  final String platform;
+
+  Map<String, String> toMap() {
+    return <String, String> {
+      'pluginName': plugin.name,
+      'platform': platform,
+      'dartClass': plugin.pluginDartClassPlatforms[platform],
+    };
+  }
+}
+
+/// Resolves the platform implementation for Dart-only plugins.
+///
+///   * If there are multiple direct pub dependencies on packages that implement the
+///     frontend plugin for the current platform, fail.
+///   * If there is a single direct dependency on a package that implements the
+///     frontend plugin for the target platform, this package is the selected implementation.
+///   * If there is no direct dependency on a package that implements the frontend
+///     plugin for the target platform, and the frontend plugin has a default implementation
+///     for the target platform the default implementation is selected.
+///   * Else fail.
+///
+///  For more details, https://flutter.dev/go/federated-plugins.
+List<PluginInterfaceResolution> resolvePlatformImplementation(
+  List<Plugin> plugins, {
+  bool throwOnPluginPubspecError = true,
+}) {
+  final List<String> platforms = <String>[
+    LinuxPlugin.kConfigKey,
+    MacOSPlugin.kConfigKey,
+    WindowsPlugin.kConfigKey,
+  ];
+  final Map<String, PluginInterfaceResolution> directDependencyResolutions
+      = <String, PluginInterfaceResolution>{};
+  final Map<String, String> defaultImplementations = <String, String>{};
+  bool didFindError = false;
+
+  for (final Plugin plugin in plugins) {
+    for (final String platform in platforms) {
+      // The plugin doesn't implement this platform.
+      if (plugin.platforms[platform] == null &&
+          plugin.defaultPackagePlatforms[platform] == null) {
+        continue;
+      }
+      // The plugin doesn't implement an interface, verify that it has a default implementation.
+      if (plugin.implementsPackage == null || plugin.implementsPackage.isEmpty) {
+        final String defaultImplementation = plugin.defaultPackagePlatforms[platform];
+        if (defaultImplementation == null) {
+          globals.printError(
+            'Plugin `${plugin.name}` doesn\'t implement a plugin interface, nor sets '
+            'a default implementation in pubspec.yaml.\n\n'
+            'To set a default implementation, use:\n'
+            'flutter:\n'
+            '  plugin:\n'
+            '    platforms:\n'
+            '      $platform:\n'
+            '        $kDefaultPackage: <plugin-implementation>\n'
+            '\n'
+            'To implement an interface, use:\n'
+            'flutter:\n'
+            '  plugin:\n'
+            '    implements: <plugin-interface>'
+            '\n'
+          );
+          didFindError = true;
+          continue;
+        }
+        defaultImplementations['$platform/${plugin.name}'] = defaultImplementation;
+        continue;
+      }
+      if (plugin.pluginDartClassPlatforms[platform] == null ||
+          plugin.pluginDartClassPlatforms[platform] == 'none') {
+        continue;
+      }
+      final String resolutionKey = '$platform/${plugin.implementsPackage}';
+      if (directDependencyResolutions.containsKey(resolutionKey)) {
+        final PluginInterfaceResolution currResolution = directDependencyResolutions[resolutionKey];
+        if (currResolution.plugin.isDirectDependency && plugin.isDirectDependency) {
+          globals.printError(
+            'Plugin `${plugin.name}` implements an interface for `$platform`, which was already '
+            'implemented by plugin `${currResolution.plugin.name}`.\n'
+            'To fix this issue, remove either dependency from pubspec.yaml.'
+            '\n\n'
+          );
+          didFindError = true;
+        }
+        if (currResolution.plugin.isDirectDependency) {
+          // Use the plugin implementation added by the user as a direct dependency.
+          continue;
+        }
+      }
+      directDependencyResolutions[resolutionKey] = PluginInterfaceResolution(
+        plugin: plugin,
+        platform: platform,
+      );
+    }
+  }
+  if (didFindError && throwOnPluginPubspecError) {
+    throwToolExit('Please resolve the errors');
+  }
+  final List<PluginInterfaceResolution> finalResolution = <PluginInterfaceResolution>[];
+  for (final MapEntry<String, PluginInterfaceResolution> resolution in directDependencyResolutions.entries) {
+    if (resolution.value.plugin.isDirectDependency) {
+      finalResolution.add(resolution.value);
+    } else if (defaultImplementations.containsKey(resolution.key)) {
+      // Pick the default implementation.
+      if (defaultImplementations[resolution.key] == resolution.value.plugin.name) {
+        finalResolution.add(resolution.value);
+      }
+    }
+  }
+  return finalResolution;
+}
+
 // Key strings for the .flutter-plugins-dependencies file.
 const String _kFlutterPluginsPluginListKey = 'plugins';
 const String _kFlutterPluginsNameKey = 'name';
@@ -684,6 +921,63 @@
   );
 }
 
+/// Generates the Dart plugin registrant, which allows to bind a platform
+/// implementation of a Dart only plugin to its interface.
+/// The new entrypoint wraps [currentMainUri], adds a [_registerPlugins] function,
+/// and writes the file to [newMainDart].
+///
+/// [mainFile] is the main entrypoint file. e.g. /<app>/lib/main.dart.
+///
+/// Returns [true] if it's necessary to create a plugin registrant, and
+/// if the new entrypoint was written to disk.
+///
+/// For more details, see https://flutter.dev/go/federated-plugins.
+Future<bool> generateMainDartWithPluginRegistrant(
+  FlutterProject rootProject,
+  PackageConfig packageConfig,
+  String currentMainUri,
+  File newMainDart,
+  File mainFile,
+) async {
+  final List<Plugin> plugins = await findPlugins(rootProject);
+  final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(
+    plugins,
+    // TODO(egarciad): Turn this on after fixing the pubspec.yaml of the plugins used in tests.
+    throwOnPluginPubspecError: false,
+  );
+  final LanguageVersion entrypointVersion = determineLanguageVersion(
+    mainFile,
+    packageConfig.packageOf(mainFile.absolute.uri),
+  );
+  final Map<String, dynamic> templateContext = <String, dynamic>{
+    'mainEntrypoint': currentMainUri,
+    'dartLanguageVersion': entrypointVersion.toString(),
+    LinuxPlugin.kConfigKey: <dynamic>[],
+    MacOSPlugin.kConfigKey: <dynamic>[],
+    WindowsPlugin.kConfigKey: <dynamic>[],
+  };
+  bool didFindPlugin = false;
+  for (final PluginInterfaceResolution resolution in resolutions) {
+    assert(templateContext.containsKey(resolution.platform));
+    (templateContext[resolution.platform] as List<dynamic>).add(resolution.toMap());
+    didFindPlugin = true;
+  }
+  if (!didFindPlugin) {
+    return false;
+  }
+  try {
+    _renderTemplateToFile(
+      _dartPluginRegistryForDesktopTemplate,
+      templateContext,
+      newMainDart.path,
+    );
+    return true;
+  } on FileSystemException catch (error) {
+    throwToolExit('Unable to write ${newMainDart.path}, received error: $error');
+    return false;
+  }
+}
+
 const String _objcPluginRegistryHeaderTemplate = '''
 //
 //  Generated file. Do not edit.
@@ -777,7 +1071,7 @@
 end
 ''';
 
-const String _dartPluginRegistryTemplate = '''
+const String _dartPluginRegistryForWebTemplate = '''
 //
 // Generated file. Do not edit.
 //
@@ -799,6 +1093,47 @@
 }
 ''';
 
+// TODO(egarciad): Evaluate merging the web and desktop plugin registry templates.
+const String _dartPluginRegistryForDesktopTemplate = '''
+//
+// Generated file. Do not edit.
+//
+
+// @dart = {{dartLanguageVersion}}
+
+import '{{mainEntrypoint}}' as entrypoint;
+import 'dart:io'; // ignore: dart_io_import.
+{{#linux}}
+import 'package:{{pluginName}}/{{pluginName}}.dart';
+{{/linux}}
+{{#macos}}
+import 'package:{{pluginName}}/{{pluginName}}.dart';
+{{/macos}}
+{{#windows}}
+import 'package:{{pluginName}}/{{pluginName}}.dart';
+{{/windows}}
+
+@pragma('vm:entry-point')
+void _registerPlugins() {
+  if (Platform.isLinux) {
+    {{#linux}}
+      {{dartClass}}.registerWith();
+    {{/linux}}
+  } else if (Platform.isMacOS) {
+    {{#macos}}
+      {{dartClass}}.registerWith();
+    {{/macos}}
+  } else if (Platform.isWindows) {
+    {{#windows}}
+      {{dartClass}}.registerWith();
+    {{/windows}}
+  }
+}
+void main() {
+  entrypoint.main();
+}
+''';
+
 const String _cppPluginRegistryHeaderTemplate = '''
 //
 //  Generated file. Do not edit.
@@ -1040,7 +1375,7 @@
     return ErrorHandlingFileSystem.deleteIfExists(file);
   } else {
     _renderTemplateToFile(
-      _dartPluginRegistryTemplate,
+      _dartPluginRegistryForWebTemplate,
       context,
       filePath,
     );
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart
index 28f5258..fba74ff 100644
--- a/packages/flutter_tools/lib/src/resident_runner.dart
+++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -920,6 +920,7 @@
       outputDir: globals.fs.directory(getBuildDirectory()),
       processManager: globals.processManager,
       projectDir: globals.fs.currentDirectory,
+      generateDartPluginRegistry: true,
     );
     _lastBuild = await globals.buildSystem.buildIncremental(
       const GenerateLocalizationsTarget(),
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart
index 7d6b714..ebfbdc7 100644
--- a/packages/flutter_tools/lib/src/run_hot.dart
+++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -26,6 +26,7 @@
 import 'device.dart';
 import 'features.dart';
 import 'globals.dart' as globals;
+import 'project.dart';
 import 'reporting/reporting.dart';
 import 'resident_devtools_handler.dart';
 import 'resident_runner.dart';
@@ -314,6 +315,15 @@
     bool enableDevTools = false,
     String route,
   }) async {
+    File mainFile = globals.fs.file(mainPath);
+    // `generated_main.dart` contains the Dart plugin registry.
+    final Directory buildDir = FlutterProject.current()
+        .directory
+        .childDirectory(globals.fs.path.join('.dart_tool', 'flutter_build'));
+    final File newMainDart = buildDir?.childFile('generated_main.dart');
+    if (newMainDart != null && newMainDart.existsSync()) {
+      mainFile = newMainDart;
+    }
     firstBuildTime = DateTime.now();
 
     final List<Future<bool>> startupTasks = <Future<bool>>[];
@@ -326,7 +336,7 @@
       if (device.generator != null) {
         startupTasks.add(
           device.generator.recompile(
-            globals.fs.file(mainPath).uri,
+            mainFile.uri,
             <Uri>[],
             // When running without a provided applicationBinary, the tool will
             // simultaneously run the initial frontend_server compilation and
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index 7bd5f73..a975e15 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -1153,6 +1153,7 @@
         outputDir: globals.fs.directory(getBuildDirectory()),
         processManager: globals.processManager,
         projectDir: project.directory,
+        generateDartPluginRegistry: true,
       );
 
       await generateLocalizationsSyntheticPackage(
diff --git a/packages/flutter_tools/lib/src/web/compile.dart b/packages/flutter_tools/lib/src/web/compile.dart
index 4edfb19..7bd7772 100644
--- a/packages/flutter_tools/lib/src/web/compile.dart
+++ b/packages/flutter_tools/lib/src/web/compile.dart
@@ -69,6 +69,8 @@
         ? null
         : globals.flutterVersion.engineRevision,
       flutterRootDir: globals.fs.directory(Cache.flutterRoot),
+      // Web uses a different Dart plugin registry.
+      generateDartPluginRegistry: false,
     ));
     if (!result.success) {
       for (final ExceptionMeasurement measurement in result.exceptions.values) {
diff --git a/packages/flutter_tools/test/general.shard/android/deferred_components_setup_validator_test.dart b/packages/flutter_tools/test/general.shard/android/deferred_components_setup_validator_test.dart
index 683c86e..eaf3914 100644
--- a/packages/flutter_tools/test/general.shard/android/deferred_components_setup_validator_test.dart
+++ b/packages/flutter_tools/test/general.shard/android/deferred_components_setup_validator_test.dart
@@ -36,6 +36,7 @@
       logger: logger,
       processManager: globals.processManager,
       engineVersion: 'invalidEngineVersion',
+      generateDartPluginRegistry: false,
     );
     return result;
   }
diff --git a/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart b/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart
index 3158bc4..2cbf0c8 100644
--- a/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart
+++ b/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart
@@ -220,6 +220,39 @@
     );
 
     expect(plugin.platforms, <String, PluginPlatform>{});
+    expect(plugin.defaultPackagePlatforms, <String, String>{
+      'linux': 'sample_package_linux',
+      'macos': 'sample_package_macos',
+      'windows': 'sample_package_windows',
+    });
+    expect(plugin.pluginDartClassPlatforms, <String, String>{});
+  });
+
+  testWithoutContext('Desktop plugin parsing allows a dartPluginClass field', () {
+    final FileSystem fileSystem = MemoryFileSystem.test();
+    const String pluginYamlRaw =
+      'platforms:\n'
+      ' linux:\n'
+      '  dartPluginClass: LinuxClass\n'
+      ' macos:\n'
+      '  dartPluginClass: MacOSClass\n'
+      ' windows:\n'
+      '  dartPluginClass: WindowsClass\n';
+
+    final YamlMap pluginYaml = loadYaml(pluginYamlRaw) as YamlMap;
+    final Plugin plugin = Plugin.fromYaml(
+      _kTestPluginName,
+      _kTestPluginPath,
+      pluginYaml,
+      const <String>[],
+      fileSystem: fileSystem,
+    );
+
+    expect(plugin.pluginDartClassPlatforms, <String, String>{
+      'linux': 'LinuxClass',
+      'macos': 'MacOSClass',
+      'windows': 'WindowsClass',
+    });
   });
 
   testWithoutContext('Plugin parsing throws a fatal error on an empty plugin', () {
diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart
index 3423d35..137c7b7 100644
--- a/packages/flutter_tools/test/general.shard/plugins_test.dart
+++ b/packages/flutter_tools/test/general.shard/plugins_test.dart
@@ -14,6 +14,8 @@
 import 'package:flutter_tools/src/base/time.dart';
 import 'package:flutter_tools/src/base/utils.dart';
 import 'package:flutter_tools/src/features.dart';
+import 'package:flutter_tools/src/dart/package_map.dart';
+import 'package:flutter_tools/src/flutter_manifest.dart';
 import 'package:flutter_tools/src/globals.dart' as globals;
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
 import 'package:flutter_tools/src/plugins.dart';
@@ -21,6 +23,7 @@
 import 'package:flutter_tools/src/version.dart';
 import 'package:meta/meta.dart';
 import 'package:mockito/mockito.dart';
+import 'package:package_config/package_config.dart';
 import 'package:yaml/yaml.dart';
 
 import '../src/common.dart';
@@ -32,6 +35,7 @@
   group('plugins', () {
     FileSystem fs;
     MockFlutterProject flutterProject;
+    MockFlutterManifest flutterManifest;
     MockIosProject iosProject;
     MockMacOSProject macosProject;
     MockAndroidProject androidProject;
@@ -48,6 +52,12 @@
     // Adds basic properties to the flutterProject and its subprojects.
     void setUpProject(FileSystem fileSystem) {
       flutterProject = MockFlutterProject();
+
+      flutterManifest = MockFlutterManifest();
+      when(flutterManifest.dependencies).thenReturn(<String>{});
+
+      when(flutterProject.manifest).thenReturn(flutterManifest);
+
       when(flutterProject.directory).thenReturn(fileSystem.systemTempDirectory.childDirectory('app'));
       // TODO(franciscojma): Remove logic for .flutter-plugins once it's deprecated.
       when(flutterProject.flutterPluginsFile).thenReturn(flutterProject.directory.childFile('.flutter-plugins'));
@@ -1297,6 +1307,767 @@
       });
     });
 
+    group('resolvePlatformImplementation', () {
+      test('selects implementation from direct dependency', () async {
+        final FileSystem fs = MemoryFileSystem();
+        final Set<String> directDependencies = <String>{
+          'url_launcher_linux',
+          'url_launcher_macos',
+        };
+        final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(<Plugin>[
+          Plugin.fromYaml(
+            'url_launcher_linux',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'implements': 'url_launcher',
+              'platforms': <String, dynamic>{
+                'linux': <String, dynamic>{
+                  'dartPluginClass': 'UrlLauncherPluginLinux',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+          Plugin.fromYaml(
+            'url_launcher_macos',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'implements': 'url_launcher',
+              'platforms': <String, dynamic>{
+                'macos': <String, dynamic>{
+                  'dartPluginClass': 'UrlLauncherPluginMacOS',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+          Plugin.fromYaml(
+            'undirect_dependency_plugin',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'implements': 'url_launcher',
+              'platforms': <String, dynamic>{
+                'windows': <String, dynamic>{
+                  'dartPluginClass': 'UrlLauncherPluginWindows',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+        ]);
+
+        resolvePlatformImplementation(<Plugin>[
+          Plugin.fromYaml(
+            'url_launcher_macos',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'implements': 'url_launcher',
+              'platforms': <String, dynamic>{
+                'macos': <String, dynamic>{
+                  'dartPluginClass': 'UrlLauncherPluginMacOS',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+        ]);
+
+        expect(resolutions.length, equals(2));
+        expect(resolutions[0].toMap(), equals(
+          <String, String>{
+            'pluginName': 'url_launcher_linux',
+            'dartClass': 'UrlLauncherPluginLinux',
+            'platform': 'linux',
+          })
+        );
+        expect(resolutions[1].toMap(), equals(
+          <String, String>{
+            'pluginName': 'url_launcher_macos',
+            'dartClass': 'UrlLauncherPluginMacOS',
+            'platform': 'macos',
+          })
+        );
+      });
+
+      test('selects default implementation', () async {
+        final FileSystem fs = MemoryFileSystem();
+        final Set<String> directDependencies = <String>{};
+
+        final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(<Plugin>[
+          Plugin.fromYaml(
+            'url_launcher',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'platforms': <String, dynamic>{
+                'linux': <String, dynamic>{
+                  'default_package': 'url_launcher_linux',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+          Plugin.fromYaml(
+            'url_launcher_linux',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'implements': 'url_launcher',
+              'platforms': <String, dynamic>{
+                'linux': <String, dynamic>{
+                  'dartPluginClass': 'UrlLauncherPluginLinux',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+        ]);
+        expect(resolutions.length, equals(1));
+        expect(resolutions[0].toMap(), equals(
+          <String, String>{
+            'pluginName': 'url_launcher_linux',
+            'dartClass': 'UrlLauncherPluginLinux',
+            'platform': 'linux',
+          })
+        );
+      });
+
+      test('selects default implementation if interface is direct dependency', () async {
+        final FileSystem fs = MemoryFileSystem();
+        final Set<String> directDependencies = <String>{'url_launcher'};
+
+        final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(<Plugin>[
+          Plugin.fromYaml(
+            'url_launcher',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'platforms': <String, dynamic>{
+                'linux': <String, dynamic>{
+                  'default_package': 'url_launcher_linux',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+          Plugin.fromYaml(
+            'url_launcher_linux',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'implements': 'url_launcher',
+              'platforms': <String, dynamic>{
+                'linux': <String, dynamic>{
+                  'dartPluginClass': 'UrlLauncherPluginLinux',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+        ]);
+        expect(resolutions.length, equals(1));
+        expect(resolutions[0].toMap(), equals(
+          <String, String>{
+            'pluginName': 'url_launcher_linux',
+            'dartClass': 'UrlLauncherPluginLinux',
+            'platform': 'linux',
+          })
+        );
+      });
+
+      test('selects user selected implementation despites default implementation', () async {
+        final FileSystem fs = MemoryFileSystem();
+        final Set<String> directDependencies = <String>{
+          'user_selected_url_launcher_implementation',
+          'url_launcher',
+        };
+
+        final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(<Plugin>[
+          Plugin.fromYaml(
+            'url_launcher',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'platforms': <String, dynamic>{
+                'linux': <String, dynamic>{
+                  'default_package': 'url_launcher_linux',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+          Plugin.fromYaml(
+            'url_launcher_linux',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'implements': 'url_launcher',
+              'platforms': <String, dynamic>{
+                'linux': <String, dynamic>{
+                  'dartPluginClass': 'UrlLauncherPluginLinux',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+          Plugin.fromYaml(
+            'user_selected_url_launcher_implementation',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'implements': 'url_launcher',
+              'platforms': <String, dynamic>{
+                'linux': <String, dynamic>{
+                  'dartPluginClass': 'UrlLauncherPluginLinux',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+        ]);
+        expect(resolutions.length, equals(1));
+        expect(resolutions[0].toMap(), equals(
+          <String, String>{
+            'pluginName': 'user_selected_url_launcher_implementation',
+            'dartClass': 'UrlLauncherPluginLinux',
+            'platform': 'linux',
+          })
+        );
+      });
+
+      test('selects user selected implementation despites default implementation', () async {
+        final FileSystem fs = MemoryFileSystem();
+        final Set<String> directDependencies = <String>{
+          'user_selected_url_launcher_implementation',
+          'url_launcher',
+        };
+
+        final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(<Plugin>[
+          Plugin.fromYaml(
+            'url_launcher',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'platforms': <String, dynamic>{
+                'linux': <String, dynamic>{
+                  'default_package': 'url_launcher_linux',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+          Plugin.fromYaml(
+            'url_launcher_linux',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'implements': 'url_launcher',
+              'platforms': <String, dynamic>{
+                'linux': <String, dynamic>{
+                  'dartPluginClass': 'UrlLauncherPluginLinux',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+          Plugin.fromYaml(
+            'user_selected_url_launcher_implementation',
+            '',
+            YamlMap.wrap(<String, dynamic>{
+              'implements': 'url_launcher',
+              'platforms': <String, dynamic>{
+                'linux': <String, dynamic>{
+                  'dartPluginClass': 'UrlLauncherPluginLinux',
+                },
+              },
+            }),
+            <String>[],
+            fileSystem: fs,
+            appDependencies: directDependencies,
+          ),
+        ]);
+        expect(resolutions.length, equals(1));
+        expect(resolutions[0].toMap(), equals(
+          <String, String>{
+            'pluginName': 'user_selected_url_launcher_implementation',
+            'dartClass': 'UrlLauncherPluginLinux',
+            'platform': 'linux',
+          })
+        );
+      });
+
+      testUsingContext('provides error when user selected multiple implementations', () async {
+        final FileSystem fs = MemoryFileSystem();
+        final Set<String> directDependencies = <String>{
+          'url_launcher_linux_1',
+          'url_launcher_linux_2',
+        };
+        expect(() {
+          resolvePlatformImplementation(<Plugin>[
+            Plugin.fromYaml(
+              'url_launcher_linux_1',
+              '',
+              YamlMap.wrap(<String, dynamic>{
+                'implements': 'url_launcher',
+                'platforms': <String, dynamic>{
+                  'linux': <String, dynamic>{
+                    'dartPluginClass': 'UrlLauncherPluginLinux',
+                  },
+                },
+              }),
+              <String>[],
+              fileSystem: fs,
+              appDependencies: directDependencies,
+            ),
+            Plugin.fromYaml(
+              'url_launcher_linux_2',
+              '',
+              YamlMap.wrap(<String, dynamic>{
+                'implements': 'url_launcher',
+                'platforms': <String, dynamic>{
+                  'linux': <String, dynamic>{
+                    'dartPluginClass': 'UrlLauncherPluginLinux',
+                  },
+                },
+              }),
+              <String>[],
+              fileSystem: fs,
+              appDependencies: directDependencies,
+            ),
+          ]);
+
+          expect(
+            testLogger.errorText,
+            'Plugin `url_launcher_linux_2` implements an interface for `linux`, which was already implemented by plugin `url_launcher_linux_1`.\n'
+            'To fix this issue, remove either dependency from pubspec.yaml.'
+            '\n\n'
+          );
+        },
+        throwsToolExit(
+          message: 'Please resolve the errors',
+        ));
+      });
+
+      testUsingContext('provides all errors when user selected multiple implementations', () async {
+        final FileSystem fs = MemoryFileSystem();
+        final Set<String> directDependencies = <String>{
+          'url_launcher_linux_1',
+          'url_launcher_linux_2',
+        };
+        expect(() {
+          resolvePlatformImplementation(<Plugin>[
+            Plugin.fromYaml(
+              'url_launcher_linux_1',
+              '',
+              YamlMap.wrap(<String, dynamic>{
+                'implements': 'url_launcher',
+                'platforms': <String, dynamic>{
+                  'linux': <String, dynamic>{
+                    'dartPluginClass': 'UrlLauncherPluginLinux',
+                  },
+                },
+              }),
+              <String>[],
+              fileSystem: fs,
+              appDependencies: directDependencies,
+            ),
+            Plugin.fromYaml(
+              'url_launcher_linux_2',
+              '',
+              YamlMap.wrap(<String, dynamic>{
+                'implements': 'url_launcher',
+                'platforms': <String, dynamic>{
+                  'linux': <String, dynamic>{
+                    'dartPluginClass': 'UrlLauncherPluginLinux',
+                  },
+                },
+              }),
+              <String>[],
+              fileSystem: fs,
+              appDependencies: directDependencies,
+            ),
+          ]);
+
+          expect(
+            testLogger.errorText,
+            'Plugin `url_launcher_linux_2` implements an interface for `linux`, which was already implemented by plugin `url_launcher_linux_1`.\n'
+            'To fix this issue, remove either dependency from pubspec.yaml.'
+            '\n\n'
+          );
+        },
+        throwsToolExit(
+          message: 'Please resolve the errors',
+        ));
+      });
+
+      testUsingContext('provides error when plugin pubspec.yaml doesn\'t have "implementation" nor "default_implementation"', () async {
+        final FileSystem fs = MemoryFileSystem();
+        final Set<String> directDependencies = <String>{
+          'url_launcher_linux_1',
+        };
+        expect(() {
+          resolvePlatformImplementation(<Plugin>[
+            Plugin.fromYaml(
+              'url_launcher_linux_1',
+              '',
+              YamlMap.wrap(<String, dynamic>{
+                'platforms': <String, dynamic>{
+                  'linux': <String, dynamic>{
+                    'dartPluginClass': 'UrlLauncherPluginLinux',
+                  },
+                },
+              }),
+              <String>[],
+              fileSystem: fs,
+              appDependencies: directDependencies,
+            ),
+          ]);
+        },
+        throwsToolExit(
+          message: 'Please resolve the errors'
+        ));
+        expect(
+          testLogger.errorText,
+          'Plugin `url_launcher_linux_1` doesn\'t implement a plugin interface, '
+          'nor sets a default implementation in pubspec.yaml.\n\n'
+          'To set a default implementation, use:\n'
+          'flutter:\n'
+          '  plugin:\n'
+          '    platforms:\n'
+          '      linux:\n'
+          '        default_package: <plugin-implementation>\n'
+          '\n'
+          'To implement an interface, use:\n'
+          'flutter:\n'
+          '  plugin:\n'
+          '    implements: <plugin-interface>'
+          '\n\n'
+        );
+      });
+
+      testUsingContext('provides all errors when plugin pubspec.yaml doesn\'t have "implementation" nor "default_implementation"', () async {
+        final FileSystem fs = MemoryFileSystem();
+        final Set<String> directDependencies = <String>{
+          'url_launcher_linux',
+          'url_launcher_windows',
+        };
+        expect(() {
+          resolvePlatformImplementation(<Plugin>[
+            Plugin.fromYaml(
+              'url_launcher_linux',
+              '',
+              YamlMap.wrap(<String, dynamic>{
+                'platforms': <String, dynamic>{
+                  'linux': <String, dynamic>{
+                    'dartPluginClass': 'UrlLauncherPluginLinux',
+                  },
+                },
+              }),
+              <String>[],
+              fileSystem: fs,
+              appDependencies: directDependencies,
+            ),
+            Plugin.fromYaml(
+              'url_launcher_windows',
+              '',
+              YamlMap.wrap(<String, dynamic>{
+                'platforms': <String, dynamic>{
+                  'windows': <String, dynamic>{
+                    'dartPluginClass': 'UrlLauncherPluginWindows',
+                  },
+                },
+              }),
+              <String>[],
+              fileSystem: fs,
+              appDependencies: directDependencies,
+            ),
+          ]);
+        },
+        throwsToolExit(
+          message: 'Please resolve the errors'
+        ));
+        expect(
+          testLogger.errorText,
+          'Plugin `url_launcher_linux` doesn\'t implement a plugin interface, '
+          'nor sets a default implementation in pubspec.yaml.\n\n'
+          'To set a default implementation, use:\n'
+          'flutter:\n'
+          '  plugin:\n'
+          '    platforms:\n'
+          '      linux:\n'
+          '        default_package: <plugin-implementation>\n'
+          '\n'
+          'To implement an interface, use:\n'
+          'flutter:\n'
+          '  plugin:\n'
+          '    implements: <plugin-interface>'
+          '\n\n'
+          'Plugin `url_launcher_windows` doesn\'t implement a plugin interface, '
+          'nor sets a default implementation in pubspec.yaml.\n\n'
+          'To set a default implementation, use:\n'
+          'flutter:\n'
+          '  plugin:\n'
+          '    platforms:\n'
+          '      windows:\n'
+          '        default_package: <plugin-implementation>\n'
+          '\n'
+          'To implement an interface, use:\n'
+          'flutter:\n'
+          '  plugin:\n'
+          '    implements: <plugin-interface>'
+          '\n\n'
+        );
+      });
+    });
+
+    group('generateMainDartWithPluginRegistrant', () {
+      testUsingContext('Generates new entrypoint', () async {
+        when(flutterProject.isModule).thenReturn(false);
+
+        final List<Directory> directories = <Directory>[];
+        final Directory fakePubCache = fs.systemTempDirectory.childDirectory('cache');
+        final File packagesFile = flutterProject.directory
+            .childFile('.packages')
+            ..createSync(recursive: true);
+
+        final Map<String, String> plugins = <String, String>{};
+        plugins['url_launcher_macos'] = '''
+  flutter:
+    plugin:
+      implements: url_launcher
+      platforms:
+        macos:
+          dartPluginClass: MacOSPlugin
+''';
+        plugins['url_launcher_linux'] = '''
+  flutter:
+    plugin:
+      implements: url_launcher
+      platforms:
+        linux:
+          dartPluginClass: LinuxPlugin
+''';
+        plugins['url_launcher_windows'] = '''
+  flutter:
+    plugin:
+      implements: url_launcher
+      platforms:
+        windows:
+          dartPluginClass: WindowsPlugin
+''';
+        plugins['awesome_macos'] = '''
+  flutter:
+    plugin:
+      implements: awesome
+      platforms:
+        macos:
+          dartPluginClass: AwesomeMacOS
+''';
+        for (final MapEntry<String, String> entry in plugins.entries) {
+          final String name = fs.path.basename(entry.key);
+          final Directory pluginDirectory = fakePubCache.childDirectory(name);
+          packagesFile.writeAsStringSync(
+              '$name:file://${pluginDirectory.childFile('lib').uri}\n',
+              mode: FileMode.writeOnlyAppend);
+          pluginDirectory.childFile('pubspec.yaml')
+              ..createSync(recursive: true)
+              ..writeAsStringSync(entry.value);
+          directories.add(pluginDirectory);
+        }
+
+        when(flutterManifest.dependencies).thenReturn(<String>{...plugins.keys});
+
+        final Directory libDir = flutterProject.directory.childDirectory('lib');
+        libDir.createSync(recursive: true);
+
+        final File mainFile = libDir.childFile('main.dart');
+        mainFile.writeAsStringSync('''
+// @dart = 2.8
+void main() {
+}
+''');
+        final File flutterBuild = flutterProject.directory.childFile('generated_main.dart');
+        final PackageConfig packageConfig = await loadPackageConfigWithLogging(
+          flutterProject.directory.childDirectory('.dart_tool').childFile('package_config.json'),
+          logger: globals.logger,
+          throwOnError: false,
+        );
+        final bool didGenerate = await generateMainDartWithPluginRegistrant(
+          flutterProject,
+          packageConfig,
+          'package:app/main.dart',
+          flutterBuild,
+          mainFile,
+        );
+        expect(didGenerate, isTrue);
+        expect(flutterBuild.readAsStringSync(),
+            '//\n'
+            '// Generated file. Do not edit.\n'
+            '//\n'
+            '\n'
+            '// @dart = 2.8\n'
+            '\n'
+            'import \'package:app/main.dart\' as entrypoint;\n'
+            'import \'dart:io\'; // ignore: dart_io_import.\n'
+            'import \'package:url_launcher_linux${fs.path.separator}url_launcher_linux.dart\';\n'
+            'import \'package:awesome_macos/awesome_macos.dart\';\n'
+            'import \'package:url_launcher_macos${fs.path.separator}url_launcher_macos.dart\';\n'
+            'import \'package:url_launcher_windows${fs.path.separator}url_launcher_windows.dart\';\n'
+            '\n'
+            '@pragma(\'vm:entry-point\')\n'
+            'void _registerPlugins() {\n'
+            '  if (Platform.isLinux) {\n'
+            '      LinuxPlugin.registerWith();\n'
+            '  } else if (Platform.isMacOS) {\n'
+            '      AwesomeMacOS.registerWith();\n'
+            '      MacOSPlugin.registerWith();\n'
+            '  } else if (Platform.isWindows) {\n'
+            '      WindowsPlugin.registerWith();\n'
+            '  }\n'
+            '}\n'
+            'void main() {\n'
+            '  entrypoint.main();\n'
+            '}\n'
+            '',
+        );
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        ProcessManager: () => FakeProcessManager.any(),
+      });
+
+      testUsingContext('Plugin without platform support throws tool exit', () async {
+        when(flutterProject.isModule).thenReturn(false);
+
+        final List<Directory> directories = <Directory>[];
+        final Directory fakePubCache = fs.systemTempDirectory.childDirectory('cache');
+        final File packagesFile = flutterProject.directory
+            .childFile('.packages')
+            ..createSync(recursive: true);
+        final Map<String, String> plugins = <String, String>{};
+        plugins['url_launcher_macos'] = '''
+  flutter:
+    plugin:
+      implements: url_launcher
+      platforms:
+        macos:
+          invalid:
+''';
+        for (final MapEntry<String, String> entry in plugins.entries) {
+          final String name = fs.path.basename(entry.key);
+          final Directory pluginDirectory = fakePubCache.childDirectory(name);
+          packagesFile.writeAsStringSync(
+              '$name:file://${pluginDirectory.childFile('lib').uri}\n',
+              mode: FileMode.writeOnlyAppend);
+          pluginDirectory.childFile('pubspec.yaml')
+              ..createSync(recursive: true)
+              ..writeAsStringSync(entry.value);
+          directories.add(pluginDirectory);
+        }
+
+        when(flutterManifest.dependencies).thenReturn(<String>{...plugins.keys});
+
+        final Directory libDir = flutterProject.directory.childDirectory('lib');
+        libDir.createSync(recursive: true);
+
+        final File mainFile = libDir.childFile('main.dart')..writeAsStringSync('');
+        final File flutterBuild = flutterProject.directory.childFile('generated_main.dart');
+        final PackageConfig packageConfig = await loadPackageConfigWithLogging(
+          flutterProject.directory.childDirectory('.dart_tool').childFile('package_config.json'),
+          logger: globals.logger,
+          throwOnError: false,
+        );
+        await expectLater(
+          generateMainDartWithPluginRegistrant(
+            flutterProject,
+            packageConfig,
+            'package:app/main.dart',
+            flutterBuild,
+            mainFile,
+          ), throwsToolExit(message:
+            'Invalid plugin specification url_launcher_macos.\n'
+            'Invalid "macos" plugin specification.'
+          ),
+        );
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        ProcessManager: () => FakeProcessManager.any(),
+      });
+
+      testUsingContext('Plugin with platform support without dart plugin class throws tool exit', () async {
+        when(flutterProject.isModule).thenReturn(false);
+
+        final List<Directory> directories = <Directory>[];
+        final Directory fakePubCache = fs.systemTempDirectory.childDirectory('cache');
+        final File packagesFile = flutterProject.directory
+            .childFile('.packages')
+            ..createSync(recursive: true);
+        final Map<String, String> plugins = <String, String>{};
+        plugins['url_launcher_macos'] = '''
+  flutter:
+    plugin:
+      implements: url_launcher
+''';
+        for (final MapEntry<String, String> entry in plugins.entries) {
+          final String name = fs.path.basename(entry.key);
+          final Directory pluginDirectory = fakePubCache.childDirectory(name);
+          packagesFile.writeAsStringSync(
+              '$name:file://${pluginDirectory.childFile('lib').uri}\n',
+              mode: FileMode.writeOnlyAppend);
+          pluginDirectory.childFile('pubspec.yaml')
+              ..createSync(recursive: true)
+              ..writeAsStringSync(entry.value);
+          directories.add(pluginDirectory);
+        }
+
+        when(flutterManifest.dependencies).thenReturn(<String>{...plugins.keys});
+
+        final Directory libDir = flutterProject.directory.childDirectory('lib');
+        libDir.createSync(recursive: true);
+
+        final File mainFile = libDir.childFile('main.dart')..writeAsStringSync('');
+        final File flutterBuild = flutterProject.directory.childFile('generated_main.dart');
+        final PackageConfig packageConfig = await loadPackageConfigWithLogging(
+          flutterProject.directory.childDirectory('.dart_tool').childFile('package_config.json'),
+          logger: globals.logger,
+          throwOnError: false,
+        );
+        await expectLater(
+          generateMainDartWithPluginRegistrant(
+            flutterProject,
+            packageConfig,
+            'package:app/main.dart',
+            flutterBuild,
+            mainFile,
+          ), throwsToolExit(message:
+            'Invalid plugin specification url_launcher_macos.\n'
+            'Cannot find the `flutter.plugin.platforms` key in the `pubspec.yaml` file. '
+            'An instruction to format the `pubspec.yaml` can be found here: '
+            'https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms'
+          ),
+        );
+      }, overrides: <Type, Generator>{
+        FileSystem: () => fs,
+        ProcessManager: () => FakeProcessManager.any(),
+      });
+    });
+
     group('pubspec', () {
 
       Directory projectDir;
@@ -1396,6 +2167,7 @@
 }
 
 class MockAndroidProject extends Mock implements AndroidProject {}
+class MockFlutterManifest extends Mock implements FlutterManifest {}
 class MockFlutterProject extends Mock implements FlutterProject {}
 class MockIosProject extends Mock implements IosProject {}
 class MockMacOSProject extends Mock implements MacOSProject {}
diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart
index 64b194a..729ddff 100644
--- a/packages/flutter_tools/test/general.shard/project_test.dart
+++ b/packages/flutter_tools/test/general.shard/project_test.dart
@@ -110,6 +110,17 @@
         );
       });
 
+      _testInMemory('reads dependencies from pubspec.yaml', () async {
+        final Directory directory = globals.fs.directory('myproject');
+        directory.childFile('pubspec.yaml')
+          ..createSync(recursive: true)
+          ..writeAsStringSync(validPubspecWithDependencies);
+        expect(
+          FlutterProject.fromDirectory(directory).manifest.dependencies,
+          <String>{'plugin_a', 'plugin_b'},
+        );
+      });
+
       _testInMemory('sets up location', () async {
         final Directory directory = globals.fs.directory('myproject');
         expect(
@@ -905,6 +916,16 @@
 flutter:
 ''';
 
+String get validPubspecWithDependencies => '''
+name: hello
+flutter:
+
+dependencies:
+  plugin_a:
+  plugin_b:
+''';
+
+
 String get invalidPubspec => '''
 name: hello
 flutter: