From b7d4806243a4e906bf061f79a0e314ba28111aa6 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Fri, 19 Feb 2021 09:22:45 -0800 Subject: [PATCH] Implement dartPluginClass support for plugins (#74469) --- .../bin/tasks/dart_plugin_registry_test.dart | 10 + .../lib/tasks/dart_plugin_registry_tests.dart | 181 ++++ .../lib/src/build_system/build_system.dart | 10 + .../lib/src/build_system/targets/common.dart | 2 + packages/flutter_tools/lib/src/bundle.dart | 1 + .../lib/src/commands/assemble.dart | 3 +- .../lib/src/commands/build_ios_framework.dart | 1 + .../lib/src/commands/packages.dart | 2 + packages/flutter_tools/lib/src/compile.dart | 23 +- packages/flutter_tools/lib/src/devfs.dart | 14 + .../lib/src/flutter_manifest.dart | 7 + .../lib/src/platform_plugins.dart | 37 +- packages/flutter_tools/lib/src/plugins.dart | 353 +++++++- .../lib/src/resident_runner.dart | 1 + packages/flutter_tools/lib/src/run_hot.dart | 12 +- .../lib/src/runner/flutter_command.dart | 1 + .../flutter_tools/lib/src/web/compile.dart | 2 + ...erred_components_setup_validator_test.dart | 1 + .../general.shard/plugin_parsing_test.dart | 33 + .../test/general.shard/plugins_test.dart | 772 ++++++++++++++++++ .../test/general.shard/project_test.dart | 21 + 21 files changed, 1466 insertions(+), 21 deletions(-) create mode 100644 dev/devicelab/bin/tasks/dart_plugin_registry_test.dart create mode 100644 dev/devicelab/lib/tasks/dart_plugin_registry_tests.dart 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 0000000000000..50c50aebcbc8e --- /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 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 0000000000000..79fcb1e83a055 --- /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 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: [ + '--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: [ + '--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: [ + '--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', ['-d', 'macos', '-v']), + environment: null, + ); + Completer registryExecutedCompleter = Completer(); + final StreamSubscription subscription = run.stdout + .transform(utf8.decoder) + .transform(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(); + + 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 48013e80e0573..9abddf1bac54b 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 @@ class Environment { @required Artifacts artifacts, @required ProcessManager processManager, @required String engineVersion, + @required bool generateDartPluginRegistry, Directory buildDir, Map defines = const {}, Map inputs = const {}, @@ -347,6 +348,7 @@ class Environment { processManager: processManager, engineVersion: engineVersion, inputs: inputs, + generateDartPluginRegistry: generateDartPluginRegistry, ); } @@ -363,6 +365,7 @@ class Environment { Map defines = const {}, Map inputs = const {}, 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. 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 cf25631047f32..7cc6f9637fd2d 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 @@ 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(); diff --git a/packages/flutter_tools/lib/src/bundle.dart b/packages/flutter_tools/lib/src/bundle.dart index 8263c32a51b4f..f89d00b289e4c 100644 --- a/packages/flutter_tools/lib/src/bundle.dart +++ b/packages/flutter_tools/lib/src/bundle.dart @@ -160,6 +160,7 @@ Future buildWithAssemble({ 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 d7e72d74be1ec..322b8606a059e 100644 --- a/packages/flutter_tools/lib/src/commands/assemble.dart +++ b/packages/flutter_tools/lib/src/commands/assemble.dart @@ -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; } 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 aed1d63eb6bae..b78381fbde18b 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 @@ end 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 606ae98d5ad5b..aa40ccf82d96b 100644 --- a/packages/flutter_tools/lib/src/commands/packages.dart +++ b/packages/flutter_tools/lib/src/commands/packages.dart @@ -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( diff --git a/packages/flutter_tools/lib/src/compile.dart b/packages/flutter_tools/lib/src/compile.dart index 76923c2746867..1f9f584b9df67 100644 --- a/packages/flutter_tools/lib/src/compile.dart +++ b/packages/flutter_tools/lib/src/compile.dart @@ -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 command = [ engineDartPath, '--disable-dart-dev', @@ -579,7 +599,6 @@ class DefaultResidentCompiler implements ResidentCompiler { if (!_controller.hasListener) { _controller.stream.listen(_handleCompilationRequest); } - final Completer completer = Completer(); _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 6efb7dea2a3f8..ef16b4fdcaeb0 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -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 pendingCompilerOutput = generator.recompile( diff --git a/packages/flutter_tools/lib/src/flutter_manifest.dart b/packages/flutter_tools/lib/src/flutter_manifest.dart index fa30fe7a12e7d..05c8c071c8130 100644 --- a/packages/flutter_tools/lib/src/flutter_manifest.dart +++ b/packages/flutter_tools/lib/src/flutter_manifest.dart @@ -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 get dependencies { + final YamlMap dependencies = _descriptor['dependencies'] as YamlMap; + return dependencies != null ? {...dependencies.keys.cast()} : {}; + } + // 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 d41fa7426e2dc..452a6260783a5 100644 --- a/packages/flutter_tools/lib/src/platform_plugins.dart +++ b/packages/flutter_tools/lib/src/platform_plugins.dart @@ -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 { '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, }; } } diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index 42c31fcdfdcd9..81751a50a5a13 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -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 dependencies, { @required FileSystem fileSystem, + Set appDependencies, }) { final List 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 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: { + if (defaultPackageForLinux != null) + LinuxPlugin.kConfigKey : defaultPackageForLinux, + if (defaultPackageForMacOS != null) + MacOSPlugin.kConfigKey : defaultPackageForMacOS, + if (defaultPackageForWindows != null) + WindowsPlugin.kConfigKey : defaultPackageForWindows, + }, + pluginDartClassPlatforms: { + 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 dependencies, FileSystem fileSystem, + bool isDirectDependency, ) { final Map platforms = {}; final String pluginClass = pluginYaml['pluginClass'] as String; @@ -178,7 +239,10 @@ class Plugin { name: name, path: path, platforms: platforms, + defaultPackagePlatforms: {}, + pluginDartClassPlatforms: {}, 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 dependencies; /// This is a mapping from platform config key to the plugin platform spec. final Map platforms; + + /// This is a mapping from platform config key to the default package implementation. + final Map defaultPackagePlatforms; + + /// This is a mapping from platform config key to the plugin class for the given platform. + final Map 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 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 ? [] : [...dependencies.keys.cast()], fileSystem: globals.fs, + appDependencies: appDependencies, ); } @@ -360,7 +469,11 @@ Future> 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> 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 toMap() { + return { + '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 resolvePlatformImplementation( + List plugins, { + bool throwOnPluginPubspecError = true, +}) { + final List platforms = [ + LinuxPlugin.kConfigKey, + MacOSPlugin.kConfigKey, + WindowsPlugin.kConfigKey, + ]; + final Map directDependencyResolutions + = {}; + final Map defaultImplementations = {}; + 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: \n' + '\n' + 'To implement an interface, use:\n' + 'flutter:\n' + ' plugin:\n' + ' implements: ' + '\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 finalResolution = []; + for (final MapEntry 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 _writeAndroidPluginRegistrant(FlutterProject project, List ); } +/// 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. //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 generateMainDartWithPluginRegistrant( + FlutterProject rootProject, + PackageConfig packageConfig, + String currentMainUri, + File newMainDart, + File mainFile, +) async { + final List plugins = await findPlugins(rootProject); + final List 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 templateContext = { + 'mainEntrypoint': currentMainUri, + 'dartLanguageVersion': entrypointVersion.toString(), + LinuxPlugin.kConfigKey: [], + MacOSPlugin.kConfigKey: [], + WindowsPlugin.kConfigKey: [], + }; + bool didFindPlugin = false; + for (final PluginInterfaceResolution resolution in resolutions) { + assert(templateContext.containsKey(resolution.platform)); + (templateContext[resolution.platform] as List).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 _writeWebPluginRegistrant(FlutterProject project, List plug 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 28f52581ae5ae..fba74ffdf3ff0 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -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(), diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 7d6b7142d31d7..ebfbdc72e75e2 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -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> startupTasks = >[]; @@ -326,7 +336,7 @@ class HotRunner extends ResidentRunner { if (device.generator != null) { startupTasks.add( device.generator.recompile( - globals.fs.file(mainPath).uri, + mainFile.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 7bd5f7337efc3..a975e15572d49 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 @@ abstract class FlutterCommand extends Command { 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 4edfb19441712..7bd7772187e3b 100644 --- a/packages/flutter_tools/lib/src/web/compile.dart +++ b/packages/flutter_tools/lib/src/web/compile.dart @@ -69,6 +69,8 @@ Future 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) { 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 683c86e56d4c8..eaf39140a613f 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 @@ void main() { 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 3158bc431f65d..2cbf0c8227539 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 @@ void main() { ); expect(plugin.platforms, {}); + expect(plugin.defaultPackagePlatforms, { + 'linux': 'sample_package_linux', + 'macos': 'sample_package_macos', + 'windows': 'sample_package_windows', + }); + expect(plugin.pluginDartClassPlatforms, {}); + }); + + 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 [], + fileSystem: fileSystem, + ); + + expect(plugin.pluginDartClassPlatforms, { + '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 3423d35a9a2c6..137c7b7dbe1a1 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/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({}); + + 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 directDependencies = { + 'url_launcher_linux', + 'url_launcher_macos', + }; + final List resolutions = resolvePlatformImplementation([ + Plugin.fromYaml( + 'url_launcher_linux', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_macos', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'macos': { + 'dartPluginClass': 'UrlLauncherPluginMacOS', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'undirect_dependency_plugin', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'windows': { + 'dartPluginClass': 'UrlLauncherPluginWindows', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ]); + + resolvePlatformImplementation([ + Plugin.fromYaml( + 'url_launcher_macos', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'macos': { + 'dartPluginClass': 'UrlLauncherPluginMacOS', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ]); + + expect(resolutions.length, equals(2)); + expect(resolutions[0].toMap(), equals( + { + 'pluginName': 'url_launcher_linux', + 'dartClass': 'UrlLauncherPluginLinux', + 'platform': 'linux', + }) + ); + expect(resolutions[1].toMap(), equals( + { + 'pluginName': 'url_launcher_macos', + 'dartClass': 'UrlLauncherPluginMacOS', + 'platform': 'macos', + }) + ); + }); + + test('selects default implementation', () async { + final FileSystem fs = MemoryFileSystem(); + final Set directDependencies = {}; + + final List resolutions = resolvePlatformImplementation([ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'default_package': 'url_launcher_linux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ]); + expect(resolutions.length, equals(1)); + expect(resolutions[0].toMap(), equals( + { + 'pluginName': 'url_launcher_linux', + 'dartClass': 'UrlLauncherPluginLinux', + 'platform': 'linux', + }) + ); + }); + + test('selects default implementation if interface is direct dependency', () async { + final FileSystem fs = MemoryFileSystem(); + final Set directDependencies = {'url_launcher'}; + + final List resolutions = resolvePlatformImplementation([ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'default_package': 'url_launcher_linux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ]); + expect(resolutions.length, equals(1)); + expect(resolutions[0].toMap(), equals( + { + 'pluginName': 'url_launcher_linux', + 'dartClass': 'UrlLauncherPluginLinux', + 'platform': 'linux', + }) + ); + }); + + test('selects user selected implementation despites default implementation', () async { + final FileSystem fs = MemoryFileSystem(); + final Set directDependencies = { + 'user_selected_url_launcher_implementation', + 'url_launcher', + }; + + final List resolutions = resolvePlatformImplementation([ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'default_package': 'url_launcher_linux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'user_selected_url_launcher_implementation', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ]); + expect(resolutions.length, equals(1)); + expect(resolutions[0].toMap(), equals( + { + '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 directDependencies = { + 'user_selected_url_launcher_implementation', + 'url_launcher', + }; + + final List resolutions = resolvePlatformImplementation([ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'default_package': 'url_launcher_linux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'user_selected_url_launcher_implementation', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ]); + expect(resolutions.length, equals(1)); + expect(resolutions[0].toMap(), equals( + { + '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 directDependencies = { + 'url_launcher_linux_1', + 'url_launcher_linux_2', + }; + expect(() { + resolvePlatformImplementation([ + Plugin.fromYaml( + 'url_launcher_linux_1', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux_2', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + 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 directDependencies = { + 'url_launcher_linux_1', + 'url_launcher_linux_2', + }; + expect(() { + resolvePlatformImplementation([ + Plugin.fromYaml( + 'url_launcher_linux_1', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux_2', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + 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 directDependencies = { + 'url_launcher_linux_1', + }; + expect(() { + resolvePlatformImplementation([ + Plugin.fromYaml( + 'url_launcher_linux_1', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + 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: \n' + '\n' + 'To implement an interface, use:\n' + 'flutter:\n' + ' plugin:\n' + ' implements: ' + '\n\n' + ); + }); + + testUsingContext('provides all errors when plugin pubspec.yaml doesn\'t have "implementation" nor "default_implementation"', () async { + final FileSystem fs = MemoryFileSystem(); + final Set directDependencies = { + 'url_launcher_linux', + 'url_launcher_windows', + }; + expect(() { + resolvePlatformImplementation([ + Plugin.fromYaml( + 'url_launcher_linux', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_windows', + '', + YamlMap.wrap({ + 'platforms': { + 'windows': { + 'dartPluginClass': 'UrlLauncherPluginWindows', + }, + }, + }), + [], + 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: \n' + '\n' + 'To implement an interface, use:\n' + 'flutter:\n' + ' plugin:\n' + ' implements: ' + '\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: \n' + '\n' + 'To implement an interface, use:\n' + 'flutter:\n' + ' plugin:\n' + ' implements: ' + '\n\n' + ); + }); + }); + + group('generateMainDartWithPluginRegistrant', () { + testUsingContext('Generates new entrypoint', () async { + when(flutterProject.isModule).thenReturn(false); + + final List directories = []; + final Directory fakePubCache = fs.systemTempDirectory.childDirectory('cache'); + final File packagesFile = flutterProject.directory + .childFile('.packages') + ..createSync(recursive: true); + + final Map plugins = {}; + 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 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({...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: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Plugin without platform support throws tool exit', () async { + when(flutterProject.isModule).thenReturn(false); + + final List directories = []; + final Directory fakePubCache = fs.systemTempDirectory.childDirectory('cache'); + final File packagesFile = flutterProject.directory + .childFile('.packages') + ..createSync(recursive: true); + final Map plugins = {}; + plugins['url_launcher_macos'] = ''' + flutter: + plugin: + implements: url_launcher + platforms: + macos: + invalid: +'''; + for (final MapEntry 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({...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: { + 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 directories = []; + final Directory fakePubCache = fs.systemTempDirectory.childDirectory('cache'); + final File packagesFile = flutterProject.directory + .childFile('.packages') + ..createSync(recursive: true); + final Map plugins = {}; + plugins['url_launcher_macos'] = ''' + flutter: + plugin: + implements: url_launcher +'''; + for (final MapEntry 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({...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: { + 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 {} diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart index 64b194a433e94..729ddffb52c6f 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 @@ 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, + {'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: