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: