Implement dartPluginClass support for plugins (#74469)

This commit is contained in:
Emmanuel Garcia 2021-02-19 09:22:45 -08:00 committed by GitHub
parent d9fca66af2
commit b7d4806243
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1466 additions and 21 deletions

View file

@ -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());
}

View file

@ -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);
}
};
}

View file

@ -308,6 +308,7 @@ class Environment {
@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 @@ class Environment {
processManager: processManager,
engineVersion: engineVersion,
inputs: inputs,
generateDartPluginRegistry: generateDartPluginRegistry,
);
}
@ -363,6 +365,7 @@ class Environment {
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 @@ class Environment {
artifacts: artifacts,
processManager: processManager,
engineVersion: engineVersion,
generateDartPluginRegistry: generateDartPluginRegistry,
);
}
@ -398,6 +402,7 @@ class Environment {
@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 @@ class Environment {
/// 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.

View file

@ -279,6 +279,8 @@ class KernelSnapshot extends Target {
fileSystemScheme: fileSystemScheme,
dartDefines: decodeDartDefines(environment.defines, kDartDefines),
packageConfig: packageConfig,
buildDir: environment.buildDir,
generateDartPluginRegistry: environment.generateDartPluginRegistry,
);
if (output == null || output.errorCount != 0) {
throw Exception();

View file

@ -160,6 +160,7 @@ Future<void> buildWithAssemble({
fileSystem: globals.fs,
logger: globals.logger,
processManager: globals.processManager,
generateDartPluginRegistry: true,
);
final Target target = buildMode == BuildMode.debug
? const CopyFlutterBundle()

View file

@ -195,7 +195,8 @@ class AssembleCommand extends FlutterCommand {
processManager: globals.processManager,
engineVersion: globals.artifacts.isLocalEngine
? null
: globals.flutterVersion.engineRevision
: globals.flutterVersion.engineRevision,
generateDartPluginRegistry: true,
);
return result;
}

View file

@ -385,6 +385,7 @@ end
engineVersion: globals.artifacts.isLocalEngine
? null
: globals.flutterVersion.engineRevision,
generateDartPluginRegistry: true,
);
Target target;
// Always build debug for simulator.

View file

@ -119,6 +119,7 @@ class PackagesGetCommand extends FlutterCommand {
outputDir: globals.fs.directory(getBuildDirectory()),
processManager: globals.processManager,
projectDir: flutterProject.directory,
generateDartPluginRegistry: true,
);
await generateLocalizationsSyntheticPackage(
@ -324,6 +325,7 @@ class PackagesInteractiveGetCommand extends FlutterCommand {
outputDir: globals.fs.directory(getBuildDirectory()),
processManager: globals.processManager,
projectDir: flutterProject.directory,
generateDartPluginRegistry: true,
);
await generateLocalizationsSyntheticPackage(

View file

@ -19,6 +19,8 @@ import 'base/logger.dart';
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 @@ class KernelCompiler {
String fileSystemScheme,
String initializeFromDill,
String platformDill,
Directory buildDir,
bool generateDartPluginRegistry = false,
@required String packagesPath,
@required BuildMode buildMode,
@required bool trackWidgetCreation,
@ -227,7 +231,8 @@ class KernelCompiler {
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 @@ class KernelCompiler {
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 @@ class DefaultResidentCompiler implements ResidentCompiler {
if (!_controller.hasListener) {
_controller.stream.listen(_handleCompilationRequest);
}
final Completer<CompilerOutput> completer = Completer<CompilerOutput>();
_controller.add(
_RecompileRequest(completer, mainUri, invalidatedFiles, outputPath, packageConfig, suppressErrors)

View file

@ -518,6 +518,20 @@ class DevFS {
// 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(

View file

@ -86,6 +86,13 @@ class FlutterManifest {
/// 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;

View file

@ -16,6 +16,9 @@ const String kPluginClass = 'pluginClass';
/// 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 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin {
@required this.name,
this.pluginClass,
this.dartPluginClass,
this.defaultPackage,
});
factory MacOSPlugin.fromYaml(String name, YamlMap yaml) {
@ -220,6 +224,7 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin {
name: name,
pluginClass: pluginClass,
dartPluginClass: yaml[kDartPluginClass] as String,
defaultPackage: yaml[kDefaultPackage] as String,
);
}
@ -227,7 +232,9 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin {
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 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin {
final String name;
final String pluginClass;
final String dartPluginClass;
final String defaultPackage;
@override
bool isNative() => pluginClass != null;
@ -244,7 +252,8 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin {
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 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{
@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 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{
name: name,
pluginClass: pluginClass,
dartPluginClass: yaml[kDartPluginClass] as String,
defaultPackage: yaml[kDefaultPackage] as String,
);
}
@ -278,7 +289,9 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{
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 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{
final String name;
final String pluginClass;
final String dartPluginClass;
final String defaultPackage;
@override
bool isNative() => pluginClass != null;
@ -296,7 +310,8 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{
'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 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin {
@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 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin {
name: name,
pluginClass: pluginClass,
dartPluginClass: yaml[kDartPluginClass] as String,
defaultPackage: yaml[kDefaultPackage] as String,
);
}
@ -330,7 +347,9 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin {
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 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin {
final String name;
final String pluginClass;
final String dartPluginClass;
final String defaultPackage;
@override
bool isNative() => pluginClass != null;
@ -348,7 +368,8 @@ class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin {
'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,
};
}
}

View file

@ -17,6 +17,7 @@ import 'base/os.dart';
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 @@ class Plugin {
@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 @@ class Plugin {
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 @@ class Plugin {
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 @@ class Plugin {
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 @@ class Plugin {
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 @@ class Plugin {
name: name,
path: path,
platforms: platforms,
defaultPackagePlatforms: <String, String>{},
pluginDartClassPlatforms: <String, String>{},
dependencies: dependencies,
isDirectDependency: isDirectDependency,
);
}
@ -295,11 +359,41 @@ class Plugin {
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 @@ class Plugin {
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 @@ Plugin _pluginFromPackage(String name, Uri packageRoot) {
flutterConfig['plugin'] as YamlMap,
dependencies == null ? <String>[] : <String>[...dependencies.keys.cast<String>()],
fileSystem: globals.fs,
appDependencies: appDependencies,
);
}
@ -360,7 +469,11 @@ Future<List<Plugin>> findPlugins(FlutterProject project, { bool throwOnError = t
);
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 @@ Future<List<Plugin>> findPlugins(FlutterProject project, { bool throwOnError = t
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 @@ Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin>
);
}
/// 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 @@ Depends on all your plugins, and provides a function to register them.
end
''';
const String _dartPluginRegistryTemplate = '''
const String _dartPluginRegistryForWebTemplate = '''
//
// Generated file. Do not edit.
//
@ -799,6 +1093,47 @@ void registerPlugins(Registrar registrar) {
}
''';
// 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 @@ Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plug
return ErrorHandlingFileSystem.deleteIfExists(file);
} else {
_renderTemplateToFile(
_dartPluginRegistryTemplate,
_dartPluginRegistryForWebTemplate,
context,
filePath,
);

View file

@ -920,6 +920,7 @@ abstract class ResidentRunner {
outputDir: globals.fs.directory(getBuildDirectory()),
processManager: globals.processManager,
projectDir: globals.fs.currentDirectory,
generateDartPluginRegistry: true,
);
_lastBuild = await globals.buildSystem.buildIncremental(
const GenerateLocalizationsTarget(),

View file

@ -26,6 +26,7 @@ import 'devfs.dart';
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 @@ class HotRunner extends ResidentRunner {
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 @@ class HotRunner extends ResidentRunner {
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

View file

@ -1153,6 +1153,7 @@ abstract class FlutterCommand extends Command<void> {
outputDir: globals.fs.directory(getBuildDirectory()),
processManager: globals.processManager,
projectDir: project.directory,
generateDartPluginRegistry: true,
);
await generateLocalizationsSyntheticPackage(

View file

@ -69,6 +69,8 @@ Future<void> buildWeb(
? 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) {

View file

@ -36,6 +36,7 @@ void main() {
logger: logger,
processManager: globals.processManager,
engineVersion: 'invalidEngineVersion',
generateDartPluginRegistry: false,
);
return result;
}

View file

@ -220,6 +220,39 @@ void main() {
);
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', () {

View file

@ -14,6 +14,8 @@ import 'package:flutter_tools/src/base/platform.dart';
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/project.dart';
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 @@ void main() {
group('plugins', () {
FileSystem fs;
MockFlutterProject flutterProject;
MockFlutterManifest flutterManifest;
MockIosProject iosProject;
MockMacOSProject macosProject;
MockAndroidProject androidProject;
@ -48,6 +52,12 @@ void main() {
// 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 @@ flutter:
});
});
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 @@ flutter:
}
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 {}

View file

@ -110,6 +110,17 @@ void main() {
);
});
_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 @@ name: hello
flutter:
''';
String get validPubspecWithDependencies => '''
name: hello
flutter:
dependencies:
plugin_a:
plugin_b:
''';
String get invalidPubspec => '''
name: hello
flutter: