diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart index d154918e3d7..74daee971ca 100644 --- a/packages/flutter_tools/lib/src/flutter_plugins.dart +++ b/packages/flutter_tools/lib/src/flutter_plugins.dart @@ -106,6 +106,12 @@ Future> findPlugins(FlutterProject project, { bool throwOnError = t return plugins; } +/// Plugin resolution type to determine the injection mechanism. +enum _PluginResolutionType { + dart, + nativeOrDart, +} + // Key strings for the .flutter-plugins-dependencies file. const String _kFlutterPluginsPluginListKey = 'plugins'; const String _kFlutterPluginsNameKey = 'name'; @@ -181,9 +187,14 @@ bool _writeFlutterPluginsList( project.web.pluginConfigKey, ]; + final Map> resolvedPlatformPlugins = _resolvePluginImplementations( + plugins, + pluginResolutionType: _PluginResolutionType.nativeOrDart, + ); + final Map pluginsMap = {}; for (final String platformKey in platformKeys) { - pluginsMap[platformKey] = _createPluginMapOfPlatform(plugins, platformKey); + pluginsMap[platformKey] = _createPluginMapOfPlatform(resolvedPlatformPlugins[platformKey] ?? [], platformKey); } final Map result = {}; @@ -212,18 +223,15 @@ bool _writeFlutterPluginsList( } /// Creates a map representation of the [plugins] for those supported by [platformKey]. +/// All given [plugins] must provide an implementation for the [platformKey]. List> _createPluginMapOfPlatform( List plugins, String platformKey, ) { - final Iterable resolvedPlatformPlugins = plugins.where((Plugin p) { - return p.platforms.containsKey(platformKey); - }); - - final Set pluginNames = resolvedPlatformPlugins.map((Plugin plugin) => plugin.name).toSet(); + final Set pluginNames = plugins.map((Plugin plugin) => plugin.name).toSet(); final List> pluginInfo = >[]; - for (final Plugin plugin in resolvedPlatformPlugins) { - // This is guaranteed to be non-null due to the `where` filter above. + for (final Plugin plugin in plugins) { + assert(plugin.platforms[platformKey] != null, 'Plugin ${plugin.name} does not provide an implementation for $platformKey.'); final PluginPlatform platformPlugin = plugin.platforms[platformKey]!; pluginInfo.add({ _kFlutterPluginsNameKey: plugin.name, @@ -1051,10 +1059,9 @@ Future injectBuildTimePluginFiles( bool webPlatform = false, }) async { final List plugins = await findPlugins(project); - // Sort the plugins by name to keep ordering stable in generated files. - plugins.sort((Plugin left, Plugin right) => left.name.compareTo(right.name)); + final Map> pluginsByPlatform = _resolvePluginImplementations(plugins, pluginResolutionType: _PluginResolutionType.nativeOrDart); if (webPlatform) { - await _writeWebPluginRegistrant(project, plugins, destination); + await _writeWebPluginRegistrant(project, pluginsByPlatform[WebPlugin.kConfigKey]!, destination); } } @@ -1081,22 +1088,22 @@ Future injectPlugins( DarwinDependencyManagement? darwinDependencyManagement, }) async { final List plugins = await findPlugins(project); - // Sort the plugins by name to keep ordering stable in generated files. - plugins.sort((Plugin left, Plugin right) => left.name.compareTo(right.name)); + final Map> pluginsByPlatform = _resolvePluginImplementations(plugins, pluginResolutionType: _PluginResolutionType.nativeOrDart); + if (androidPlatform) { - await _writeAndroidPluginRegistrant(project, plugins); + await _writeAndroidPluginRegistrant(project, pluginsByPlatform[AndroidPlugin.kConfigKey]!); } if (iosPlatform) { - await _writeIOSPluginRegistrant(project, plugins); + await _writeIOSPluginRegistrant(project, pluginsByPlatform[IOSPlugin.kConfigKey]!); } if (linuxPlatform) { - await _writeLinuxPluginFiles(project, plugins); + await _writeLinuxPluginFiles(project, pluginsByPlatform[LinuxPlugin.kConfigKey]!); } if (macOSPlatform) { - await _writeMacOSPluginRegistrant(project, plugins); + await _writeMacOSPluginRegistrant(project, pluginsByPlatform[MacOSPlugin.kConfigKey]!); } if (windowsPlatform) { - await writeWindowsPluginFiles(project, plugins, globals.templateRenderer, allowedPlugins: allowedPlugins); + await writeWindowsPluginFiles(project, pluginsByPlatform[WindowsPlugin.kConfigKey]!, globals.templateRenderer, allowedPlugins: allowedPlugins); } if (iosPlatform || macOSPlatform) { final DarwinDependencyManagement darwinDependencyManagerSetup = darwinDependencyManagement ?? DarwinDependencyManagement( @@ -1130,7 +1137,7 @@ bool hasPlugins(FlutterProject project) { return _readFileContent(project.flutterPluginsFile) != null; } -/// Resolves the platform implementation for Dart-only plugins. +/// Resolves the plugin implementations for all platforms. /// /// * If there is only one dependency on a package that implements the /// frontend plugin for the current platform, use that. @@ -1142,73 +1149,62 @@ bool hasPlugins(FlutterProject project) { /// * Else fail. /// /// For more details, https://flutter.dev/go/federated-plugins. -// TODO(stuartmorgan): Expand implementation to apply to all implementations, -// not just Dart-only, per the federated plugin spec. +/// +/// If [selectDartPluginsOnly] is enabled, only Dart plugin implementations are +/// considered. Else, native and Dart plugin implementations are considered. List resolvePlatformImplementation( - List plugins, -) { - const Iterable platformKeys = [ - AndroidPlugin.kConfigKey, - IOSPlugin.kConfigKey, - LinuxPlugin.kConfigKey, - MacOSPlugin.kConfigKey, - WindowsPlugin.kConfigKey, - ]; - final List pluginResolutions = []; - bool hasResolutionError = false; - bool hasPluginPubspecError = false; + List plugins, { + required bool selectDartPluginsOnly, +}) { + final Map> resolution = _resolvePluginImplementations( + plugins, + pluginResolutionType: selectDartPluginsOnly ? _PluginResolutionType.dart : _PluginResolutionType.nativeOrDart, + ); + return resolution.entries.expand((MapEntry> entry) { + return entry.value.map((Plugin plugin) { + return PluginInterfaceResolution(plugin: plugin, platform: entry.key); + }); + }).toList(); +} - for (final String platformKey in platformKeys) { - // Key: the plugin name, value: the list of plugin candidates for the implementation of [platformKey]. - final Map> pluginImplCandidates = >{}; - - // Key: the plugin name, value: the plugin name of the default implementation of [platformKey]. - final Map defaultImplementations = {}; - - for (final Plugin plugin in plugins) { - final String? error = _validatePlugin(plugin, platformKey); - if (error != null) { - globals.printError(error); - hasPluginPubspecError = true; - continue; - } - final String? implementsPluginName = _getImplementedPlugin(plugin, platformKey); - final String? defaultImplPluginName = _getDefaultImplPlugin(plugin, platformKey); +/// Resolves the plugin implementations for all platforms, +/// see [resolvePlatformImplementation]. +/// +/// Only plugins which provide the according platform implementation are returned. +Map> _resolvePluginImplementations( + List plugins, { + required _PluginResolutionType pluginResolutionType, +}) { + final Map> pluginsByPlatform = >{ + AndroidPlugin.kConfigKey: [], + IOSPlugin.kConfigKey: [], + LinuxPlugin.kConfigKey: [], + MacOSPlugin.kConfigKey: [], + WindowsPlugin.kConfigKey: [], + WebPlugin.kConfigKey: [], + }; - if (defaultImplPluginName != null) { - // Each plugin can only have one default implementation for this [platformKey]. - defaultImplementations[plugin.name] = defaultImplPluginName; - } - if (implementsPluginName != null) { - pluginImplCandidates.putIfAbsent(implementsPluginName, () => []); - pluginImplCandidates[implementsPluginName]!.add(plugin); - } - } + bool hasPluginPubspecError = false; + bool hasResolutionError = false; - final Map pluginResolution = {}; + for (final String platformKey in pluginsByPlatform.keys) { + final ( + List platformPluginResolutions, + bool hasPlatformPluginPubspecError, + bool hasPlatformResolutionError + ) = _resolvePluginImplementationsByPlatform( + plugins, + platformKey, + pluginResolutionType: pluginResolutionType, + ); - // Now resolve all the possible resolutions to a single option for each - // plugin, or throw if that's not possible. - for (final MapEntry> implCandidatesEntry in pluginImplCandidates.entries) { - final (Plugin? resolution, String? error) = _resolveImplementationOfPlugin( - platformKey: platformKey, - pluginName: implCandidatesEntry.key, - candidates: implCandidatesEntry.value, - defaultPackageName: defaultImplementations[implCandidatesEntry.key], - ); - if (error != null) { - globals.printError(error); - hasResolutionError = true; - } else if (resolution != null) { - pluginResolution[implCandidatesEntry.key] = resolution; - } + if (hasPlatformPluginPubspecError) { + hasPluginPubspecError = true; + } else if (hasPlatformResolutionError) { + hasResolutionError = true; + } else { + pluginsByPlatform[platformKey] = platformPluginResolutions; } - - pluginResolutions.addAll( - pluginResolution.values.map((Plugin plugin) { - return PluginInterfaceResolution(plugin: plugin, platform: platformKey); - }), - ); } if (hasPluginPubspecError) { throwToolExit('Please resolve the plugin pubspec errors'); @@ -1216,14 +1212,103 @@ List resolvePlatformImplementation( if (hasResolutionError) { throwToolExit('Please resolve the plugin implementation selection errors'); } - return pluginResolutions; + return pluginsByPlatform; +} + +/// Resolves the plugins for the given [platformKey] (Dart-only or native +/// implementations). +(List pluginImplementations, bool hasPluginPubspecError, bool hasResolutionError) _resolvePluginImplementationsByPlatform( + Iterable plugins, + String platformKey, { + _PluginResolutionType pluginResolutionType = _PluginResolutionType.nativeOrDart, +}) { + bool hasPluginPubspecError = false; + bool hasResolutionError = false; + + // Key: the plugin name, value: the list of plugin candidates for the implementation of [platformKey]. + final Map> pluginImplCandidates = >{}; + + // Key: the plugin name, value: the plugin of the default implementation of [platformKey]. + final Map defaultImplementations = {}; + + for (final Plugin plugin in plugins) { + final String? error = _validatePlugin(plugin, platformKey, pluginResolutionType: pluginResolutionType); + if (error != null) { + globals.printError(error); + hasPluginPubspecError = true; + continue; + } + final String? implementsPluginName = _getImplementedPlugin(plugin, platformKey, pluginResolutionType: pluginResolutionType); + final String? defaultImplPluginName = _getDefaultImplPlugin(plugin, platformKey, pluginResolutionType: pluginResolutionType); + + if (defaultImplPluginName != null) { + final Plugin? defaultPackage = plugins.where((Plugin plugin) => plugin.name == defaultImplPluginName).firstOrNull; + if (defaultPackage != null) { + if (_hasPluginInlineImpl(defaultPackage, platformKey, pluginResolutionType: _PluginResolutionType.nativeOrDart)) { + if (pluginResolutionType == _PluginResolutionType.nativeOrDart || + _hasPluginInlineImpl(defaultPackage, platformKey, pluginResolutionType: pluginResolutionType)) { + // Each plugin can only have one default implementation for this [platformKey]. + defaultImplementations[plugin.name] = defaultPackage; + // No need to add the default plugin to `pluginImplCandidates`, + // as if the plugin is present and provides an implementation + // it is added via `_getImplementedPlugin`. + } + } else { + // Only warn, if neither an implementation for native nor for Dart is given. + globals.printWarning( + 'Package ${plugin.name}:$platformKey references $defaultImplPluginName:$platformKey as the default plugin, but it does not provide an inline implementation.\n' + 'Ask the maintainers of ${plugin.name} to either avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName` ' + 'or add an inline implementation to $defaultImplPluginName via `platforms: $platformKey:` `pluginClass` or `dartPluginClass`.\n', + ); + } + } else { + globals.printWarning( + 'Package ${plugin.name}:$platformKey references $defaultImplPluginName:$platformKey as the default plugin, but the package does not exist.\n' + 'Ask the maintainers of ${plugin.name} to either avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName` ' + 'or create a plugin named $defaultImplPluginName.\n', + ); + } + } + if (implementsPluginName != null) { + pluginImplCandidates.putIfAbsent(implementsPluginName, () => []); + pluginImplCandidates[implementsPluginName]!.add(plugin); + } + } + + // Key: the plugin name, value: the plugin which provides an implementation for [platformKey]. + final Map pluginResolution = {}; + + // Now resolve all the possible resolutions to a single option for each + // plugin, or throw if that's not possible. + for (final MapEntry> implCandidatesEntry in pluginImplCandidates.entries) { + final (Plugin? resolution, String? error) = _resolveImplementationOfPlugin( + platformKey: platformKey, + pluginResolutionType: pluginResolutionType, + pluginName: implCandidatesEntry.key, + candidates: implCandidatesEntry.value, + defaultPackage: defaultImplementations[implCandidatesEntry.key], + ); + if (error != null) { + globals.printError(error); + hasResolutionError = true; + } else if (resolution != null) { + pluginResolution[implCandidatesEntry.key] = resolution; + } + } + + // Sort the plugins by name to keep ordering stable in generated files. + final List pluginImplementations = pluginResolution.values.toList() + ..sort((Plugin left, Plugin right) => left.name.compareTo(right.name)); + return (pluginImplementations, hasPluginPubspecError, hasResolutionError); } /// Validates conflicting plugin parameters in pubspec, such as /// `dartPluginClass`, `default_package` and `implements`. /// /// Returns an error, if failing. -String? _validatePlugin(Plugin plugin, String platformKey) { +String? _validatePlugin(Plugin plugin, String platformKey, { + required _PluginResolutionType pluginResolutionType, +}) { final String? implementsPackage = plugin.implementsPackage; final String? defaultImplPluginName = plugin.defaultPackagePlatforms[platformKey]; @@ -1241,10 +1326,10 @@ String? _validatePlugin(Plugin plugin, String platformKey) { 'or avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName`.\n'; } - if (_hasPluginInlineDartImpl(plugin, platformKey)) { + if (_hasPluginInlineImpl(plugin, platformKey, pluginResolutionType: pluginResolutionType)) { return 'Plugin ${plugin.name}:$platformKey which provides an inline implementation ' 'cannot also reference a default implementation for $defaultImplPluginName. ' - 'Ask the maintainers of ${plugin.name} to either remove the implementation via `platforms: $platformKey: dartPluginClass` ' + 'Ask the maintainers of ${plugin.name} to either remove the implementation via `platforms: $platformKey:${pluginResolutionType == _PluginResolutionType.dart ? ' dartPluginClass' : '` `pluginClass` or `dartPLuginClass'}` ' 'or avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName`.\n'; } } @@ -1262,19 +1347,22 @@ String? _validatePlugin(Plugin plugin, String platformKey) { /// * The [plugin] (e.g. 'url_launcher') implements itself and then also /// serves as its own default implementation. /// * The [plugin] does not provide an implementation. -String? _getImplementedPlugin(Plugin plugin, String platformKey) { - final bool hasInlineDartImpl = _hasPluginInlineDartImpl(plugin, platformKey); +String? _getImplementedPlugin( + Plugin plugin, + String platformKey, { + _PluginResolutionType pluginResolutionType = _PluginResolutionType.nativeOrDart, +}) { + if (_hasPluginInlineImpl(plugin, platformKey, pluginResolutionType: pluginResolutionType)) { + // Only can serve, if the plugin has an inline implementation. - if (hasInlineDartImpl) { final String? implementsPackage = plugin.implementsPackage; - - // Only can serve, if the plugin has a dart inline implementation. if (implementsPackage != null && implementsPackage.isNotEmpty) { + // The inline plugin implements another package. return implementsPackage; } - if (_isEligibleDartSelfImpl(plugin, platformKey)) { - // The inline Dart plugin implements itself. + if (pluginResolutionType == _PluginResolutionType.nativeOrDart || _isEligibleDartSelfImpl(plugin, platformKey)) { + // The inline plugin implements itself. return plugin.name; } } @@ -1293,16 +1381,20 @@ String? _getImplementedPlugin(Plugin plugin, String platformKey) { /// * The [plugin] (e.g. 'url_launcher') implements itself and then also /// serves as its own default implementation. /// * The [plugin] does not reference a default implementation. -String? _getDefaultImplPlugin(Plugin plugin, String platformKey) { +String? _getDefaultImplPlugin( + Plugin plugin, + String platformKey, { + _PluginResolutionType pluginResolutionType = _PluginResolutionType.nativeOrDart, +}) { final String? defaultImplPluginName = plugin.defaultPackagePlatforms[platformKey]; if (defaultImplPluginName != null) { return defaultImplPluginName; } - if (_hasPluginInlineDartImpl(plugin, platformKey) && - _isEligibleDartSelfImpl(plugin, platformKey)) { - // The inline Dart plugin serves as its own default implementation. + if (_hasPluginInlineImpl(plugin, platformKey, pluginResolutionType: pluginResolutionType) && + (pluginResolutionType == _PluginResolutionType.nativeOrDart || _isEligibleDartSelfImpl(plugin, platformKey))) { + // The inline plugin serves as its own default implementation. return plugin.name; } @@ -1331,7 +1423,17 @@ bool _isEligibleDartSelfImpl(Plugin plugin, String platformKey) { return !isDesktop || hasMinVersionForImplementsRequirement; } -/// Determine if the plugin provides an inline dart implementation. +/// Determine if the plugin provides an inline implementation. +bool _hasPluginInlineImpl( + Plugin plugin, + String platformKey, { + required _PluginResolutionType pluginResolutionType, +}) { + return pluginResolutionType == _PluginResolutionType.nativeOrDart && plugin.platforms[platformKey] != null || + pluginResolutionType == _PluginResolutionType.dart && _hasPluginInlineDartImpl(plugin, platformKey); +} + +/// Determine if the plugin provides an inline Dart implementation. bool _hasPluginInlineDartImpl(Plugin plugin, String platformKey) { return plugin.pluginDartClassPlatforms[platformKey] != null && plugin.pluginDartClassPlatforms[platformKey] != 'none'; @@ -1343,9 +1445,10 @@ bool _hasPluginInlineDartImpl(Plugin plugin, String platformKey) { /// Returns an [error] string, if failing. (Plugin? resolution, String? error) _resolveImplementationOfPlugin({ required String platformKey, + required _PluginResolutionType pluginResolutionType, required String pluginName, required List candidates, - String? defaultPackageName, + Plugin? defaultPackage, }) { // If there's only one candidate, use it. if (candidates.length == 1) { @@ -1377,12 +1480,10 @@ bool _hasPluginInlineDartImpl(Plugin plugin, String platformKey) { } } // Next, defer to the default implementation if there is one. - if (defaultPackageName != null) { - final int defaultIndex = candidates - .indexWhere((Plugin plugin) => plugin.name == defaultPackageName); - if (defaultIndex != -1) { - return (candidates[defaultIndex], null); - } + if (defaultPackage != null && candidates.contains(defaultPackage)) { + // By definition every candidate has an inline implementation + assert(_hasPluginInlineImpl(defaultPackage, platformKey, pluginResolutionType: pluginResolutionType)); + return (defaultPackage, null); } // Otherwise, require an explicit choice. if (candidates.length > 1) { @@ -1417,6 +1518,7 @@ Future generateMainDartWithPluginRegistrant( final List plugins = await findPlugins(rootProject); final List resolutions = resolvePlatformImplementation( plugins, + selectDartPluginsOnly: true, ); final LanguageVersion entrypointVersion = determineLanguageVersion( mainFile, diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index 977708f51a1..8fc562f5236 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -142,8 +142,8 @@ class Plugin { } // TODO(stuartmorgan): Consider merging web into this common handling; the - // fact that its implementation of Dart-only plugins and default packages - // are separate is legacy. + // fact that its implementation of Dart-only plugins and default packages + // are separate is legacy. final List sharedHandlingPlatforms = [ AndroidPlugin.kConfigKey, IOSPlugin.kConfigKey, @@ -382,7 +382,7 @@ class Plugin { /// 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. + /// This is a mapping from platform config key to the Dart plugin class for the given platform. final Map pluginDartClassPlatforms; /// Whether this plugin is a direct dependency of the app. diff --git a/packages/flutter_tools/test/general.shard/dart_plugin_test.dart b/packages/flutter_tools/test/general.shard/dart_plugin_test.dart index 401959ac0c5..4e094070e59 100644 --- a/packages/flutter_tools/test/general.shard/dart_plugin_test.dart +++ b/packages/flutter_tools/test/general.shard/dart_plugin_test.dart @@ -43,40 +43,43 @@ void main() { 'url_launcher_linux', 'url_launcher_macos', }; - final List resolutions = resolvePlatformImplementation([ - Plugin.fromYaml( - 'url_launcher_linux', - '', - YamlMap.wrap({ - 'implements': 'url_launcher', - 'platforms': { - 'linux': { - 'dartPluginClass': 'UrlLauncherPluginLinux', + final List resolutions = resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher_linux', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - Plugin.fromYaml( - 'url_launcher_macos', - '', - YamlMap.wrap({ - 'implements': 'url_launcher', - 'platforms': { - 'macos': { - 'dartPluginClass': 'UrlLauncherPluginMacOS', + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_macos', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'macos': { + 'dartPluginClass': 'UrlLauncherPluginMacOS', + }, }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - ]); + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); expect(resolutions.length, equals(2)); expect(resolutions[0].toMap(), equals( @@ -139,6 +142,7 @@ void main() { appDependencies: directDependencies, ), ], + selectDartPluginsOnly: true, ); expect(resolutions.length, equals(1)); @@ -155,40 +159,43 @@ void main() { final Set directDependencies = { 'url_launcher_macos', }; - final List resolutions = resolvePlatformImplementation([ - Plugin.fromYaml( - 'url_launcher_macos', - '', - YamlMap.wrap({ - 'implements': 'url_launcher', - 'platforms': { - 'macos': { - 'dartPluginClass': 'UrlLauncherPluginMacOS', + final List resolutions = resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher_macos', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'macos': { + 'dartPluginClass': 'UrlLauncherPluginMacOS', + }, }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - Plugin.fromYaml( - 'transitive_dependency_plugin', - '', - YamlMap.wrap({ - 'implements': 'url_launcher', - 'platforms': { - 'windows': { - 'dartPluginClass': 'UrlLauncherPluginWindows', + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'transitive_dependency_plugin', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'windows': { + 'dartPluginClass': 'UrlLauncherPluginWindows', + }, }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - ]); + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); expect(resolutions.length, equals(2)); expect(resolutions[0].toMap(), equals( @@ -210,26 +217,29 @@ void main() { testWithoutContext('selects inline implementation on mobile', () async { final Set directDependencies = {}; - final List resolutions = resolvePlatformImplementation([ - Plugin.fromYaml( - 'url_launcher', - '', - YamlMap.wrap({ - 'platforms': { - 'android': { - 'dartPluginClass': 'UrlLauncherAndroid', - }, - 'ios': { - 'dartPluginClass': 'UrlLauncherIos', + final List resolutions = resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'android': { + 'dartPluginClass': 'UrlLauncherAndroid', + }, + 'ios': { + 'dartPluginClass': 'UrlLauncherIos', + }, }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - ]); + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); expect(resolutions.length, equals(2)); expect(resolutions[0].toMap(), equals( { @@ -252,29 +262,32 @@ void main() { 'missing min Flutter SDK constraint', () async { final Set directDependencies = {}; - final List resolutions = resolvePlatformImplementation([ - Plugin.fromYaml( - 'url_launcher', - '', - YamlMap.wrap({ - 'platforms': { - 'linux': { - 'dartPluginClass': 'UrlLauncherLinux', - }, - 'macos': { - 'dartPluginClass': 'UrlLauncherMacOS', - }, - 'windows': { - 'dartPluginClass': 'UrlLauncherWindows', + final List resolutions = resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherLinux', + }, + 'macos': { + 'dartPluginClass': 'UrlLauncherMacOS', + }, + 'windows': { + 'dartPluginClass': 'UrlLauncherWindows', + }, }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - ]); + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); expect(resolutions.length, equals(0)); }); @@ -283,29 +296,32 @@ void main() { 'min Flutter SDK constraint < 2.11', () async { final Set directDependencies = {}; - final List resolutions = resolvePlatformImplementation([ - Plugin.fromYaml( - 'url_launcher', - '', - YamlMap.wrap({ - 'platforms': { - 'linux': { - 'dartPluginClass': 'UrlLauncherLinux', - }, - 'macos': { - 'dartPluginClass': 'UrlLauncherMacOS', - }, - 'windows': { - 'dartPluginClass': 'UrlLauncherWindows', + final List resolutions = resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherLinux', + }, + 'macos': { + 'dartPluginClass': 'UrlLauncherMacOS', + }, + 'windows': { + 'dartPluginClass': 'UrlLauncherWindows', + }, }, - }, - }), - VersionConstraint.parse('>=2.10.0'), - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - ]); + }), + VersionConstraint.parse('>=2.10.0'), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); expect(resolutions.length, equals(0)); }); @@ -313,29 +329,32 @@ void main() { 'min Flutter SDK requirement of at least 2.11', () async { final Set directDependencies = {}; - final List resolutions = resolvePlatformImplementation([ - Plugin.fromYaml( - 'url_launcher', - '', - YamlMap.wrap({ - 'platforms': { - 'linux': { - 'dartPluginClass': 'UrlLauncherLinux', - }, - 'macos': { - 'dartPluginClass': 'UrlLauncherMacOS', - }, - 'windows': { - 'dartPluginClass': 'UrlLauncherWindows', + final List resolutions = resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherLinux', + }, + 'macos': { + 'dartPluginClass': 'UrlLauncherMacOS', + }, + 'windows': { + 'dartPluginClass': 'UrlLauncherWindows', + }, }, - }, - }), - VersionConstraint.parse('>=2.11.0'), - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - ]); + }), + VersionConstraint.parse('>=2.11.0'), + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); expect(resolutions.length, equals(3)); expect( resolutions.map((PluginInterfaceResolution resolution) => resolution.toMap()), @@ -362,269 +381,15 @@ void main() { testWithoutContext('selects default implementation', () async { final Set directDependencies = {}; - final List resolutions = resolvePlatformImplementation([ - Plugin.fromYaml( - 'url_launcher', - '', - YamlMap.wrap({ - 'platforms': { - 'linux': { - 'default_package': 'url_launcher_linux', - }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - // Include three possible implementations, one before and one after - // to ensure that the selection is working as intended, not just by - // coincidence of order. - Plugin.fromYaml( - 'another_url_launcher_linux', - '', - YamlMap.wrap({ - 'implements': 'url_launcher', - 'platforms': { - 'linux': { - 'dartPluginClass': 'UnofficialUrlLauncherPluginLinux', - }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - Plugin.fromYaml( - 'url_launcher_linux', - '', - YamlMap.wrap({ - 'implements': 'url_launcher', - 'platforms': { - 'linux': { - 'dartPluginClass': 'UrlLauncherPluginLinux', - }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - Plugin.fromYaml( - 'yet_another_url_launcher_linux', - '', - YamlMap.wrap({ - 'implements': 'url_launcher', - 'platforms': { - 'linux': { - 'dartPluginClass': 'UnofficialUrlLauncherPluginLinux2', - }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - ]); - expect(resolutions.length, equals(1)); - expect(resolutions[0].toMap(), equals( - { - 'pluginName': 'url_launcher_linux', - 'dartClass': 'UrlLauncherPluginLinux', - 'platform': 'linux', - }) - ); - }); - - testWithoutContext('selects default implementation if interface is direct dependency', () async { - final Set directDependencies = {'url_launcher'}; - - final List resolutions = resolvePlatformImplementation([ - Plugin.fromYaml( - 'url_launcher', - '', - YamlMap.wrap({ - 'platforms': { - 'linux': { - 'default_package': 'url_launcher_linux', - }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - Plugin.fromYaml( - 'url_launcher_linux', - '', - YamlMap.wrap({ - 'implements': 'url_launcher', - 'platforms': { - 'linux': { - 'dartPluginClass': 'UrlLauncherPluginLinux', - }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - ]); - expect(resolutions.length, equals(1)); - expect(resolutions[0].toMap(), equals( - { - 'pluginName': 'url_launcher_linux', - 'dartClass': 'UrlLauncherPluginLinux', - 'platform': 'linux', - }) - ); - }); - - testWithoutContext('selects user selected implementation despite default implementation', () async { - 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', - }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - Plugin.fromYaml( - 'url_launcher_linux', - '', - YamlMap.wrap({ - 'implements': 'url_launcher', - 'platforms': { - 'linux': { - 'dartPluginClass': 'UrlLauncherPluginLinux', - }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - Plugin.fromYaml( - 'user_selected_url_launcher_implementation', - '', - YamlMap.wrap({ - 'implements': 'url_launcher', - 'platforms': { - 'linux': { - 'dartPluginClass': 'UrlLauncherPluginLinux', - }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - ]); - expect(resolutions.length, equals(1)); - expect(resolutions[0].toMap(), equals( - { - 'pluginName': 'user_selected_url_launcher_implementation', - 'dartClass': 'UrlLauncherPluginLinux', - 'platform': 'linux', - }) - ); - }); - - testWithoutContext('selects user selected implementation despite inline implementation', () async { - final Set directDependencies = { - 'user_selected_url_launcher_implementation', - 'url_launcher', - }; - - final List resolutions = resolvePlatformImplementation([ - Plugin.fromYaml( - 'url_launcher', - '', - YamlMap.wrap({ - 'platforms': { - 'android': { - 'dartPluginClass': 'UrlLauncherAndroid', - }, - 'ios': { - 'dartPluginClass': 'UrlLauncherIos', - }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - Plugin.fromYaml( - 'user_selected_url_launcher_implementation', - '', - YamlMap.wrap({ - 'implements': 'url_launcher', - 'platforms': { - 'android': { - 'dartPluginClass': 'UrlLauncherAndroid', - }, - }, - }), - null, - [], - fileSystem: fs, - appDependencies: directDependencies, - ), - ]); - expect(resolutions.length, equals(2)); - expect(resolutions[0].toMap(), equals( - { - 'pluginName': 'user_selected_url_launcher_implementation', - 'dartClass': 'UrlLauncherAndroid', - 'platform': 'android', - }) - ); - expect(resolutions[1].toMap(), equals( - { - 'pluginName': 'url_launcher', - 'dartClass': 'UrlLauncherIos', - 'platform': 'ios', - }) - ); - }); - - testUsingContext( - 'provides error when a plugin has a default implementation and implements another plugin', - () async { - final Set directDependencies = { - 'url_launcher', - }; - expect(() { - resolvePlatformImplementation([ + final List resolutions = resolvePlatformImplementation( + [ Plugin.fromYaml( 'url_launcher', '', YamlMap.wrap({ 'platforms': { 'linux': { - 'default_package': 'url_launcher_linux_1', + 'default_package': 'url_launcher_linux', }, }, }), @@ -633,14 +398,17 @@ void main() { fileSystem: fs, appDependencies: directDependencies, ), + // Include three possible implementations, one before and one after + // to ensure that the selection is working as intended, not just by + // coincidence of order. Plugin.fromYaml( - 'url_launcher_linux_1', + 'another_url_launcher_linux', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { - 'default_package': 'url_launcher_linux_2', + 'dartPluginClass': 'UnofficialUrlLauncherPluginLinux', }, }, }), @@ -650,7 +418,7 @@ void main() { appDependencies: directDependencies, ), Plugin.fromYaml( - 'url_launcher_linux_2', + 'url_launcher_linux', '', YamlMap.wrap({ 'implements': 'url_launcher', @@ -665,29 +433,40 @@ void main() { fileSystem: fs, appDependencies: directDependencies, ), - ]); - }, - throwsToolExit( - message: 'Please resolve the plugin pubspec errors', - )); - - expect( - testLogger.errorText, - 'Plugin url_launcher_linux_1:linux provides an implementation for url_launcher ' - 'and also references a default implementation for url_launcher_linux_2, which is currently not supported. ' - 'Ask the maintainers of url_launcher_linux_1 to either remove the implementation via `implements: url_launcher` ' - 'or avoid referencing a default implementation via `platforms: linux: default_package: url_launcher_linux_2`.' - '\n\n'); + Plugin.fromYaml( + 'yet_another_url_launcher_linux', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UnofficialUrlLauncherPluginLinux2', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); + expect(resolutions.length, equals(1)); + expect(resolutions[0].toMap(), equals( + { + 'pluginName': 'url_launcher_linux', + 'dartClass': 'UrlLauncherPluginLinux', + 'platform': 'linux', + }) + ); }); - testUsingContext( - 'provides error when a plugin has a default implementation and an inline implementation', - () async { - final Set directDependencies = { - 'url_launcher', - }; - expect(() { - resolvePlatformImplementation([ + testWithoutContext('selects default implementation if interface is direct dependency', () async { + final Set directDependencies = {'url_launcher'}; + + final List resolutions = resolvePlatformImplementation( + [ Plugin.fromYaml( 'url_launcher', '', @@ -695,7 +474,6 @@ void main() { 'platforms': { 'linux': { 'default_package': 'url_launcher_linux', - 'dartPluginClass': 'UrlLauncherPluginLinux', }, }, }), @@ -720,30 +498,44 @@ void main() { fileSystem: fs, appDependencies: directDependencies, ), - ]); - }, - throwsToolExit( - message: 'Please resolve the plugin pubspec errors', - )); - - expect( - testLogger.errorText, - 'Plugin url_launcher:linux which provides an inline implementation ' - 'cannot also reference a default implementation for url_launcher_linux. ' - 'Ask the maintainers of url_launcher to either remove the implementation via `platforms: linux: dartPluginClass` ' - 'or avoid referencing a default implementation via `platforms: linux: default_package: url_launcher_linux`.' - '\n\n'); + ], + selectDartPluginsOnly: true, + ); + expect(resolutions.length, equals(1)); + expect(resolutions[0].toMap(), equals( + { + 'pluginName': 'url_launcher_linux', + 'dartClass': 'UrlLauncherPluginLinux', + 'platform': 'linux', + }) + ); }); - testUsingContext('provides error when user selected multiple implementations', () async { + testWithoutContext('user-selected implementation overrides default implementation', () async { final Set directDependencies = { - 'url_launcher_linux_1', - 'url_launcher_linux_2', + 'user_selected_url_launcher_implementation', + 'url_launcher', }; - expect(() { - resolvePlatformImplementation([ + + final List resolutions = resolvePlatformImplementation( + [ Plugin.fromYaml( - 'url_launcher_linux_1', + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'default_package': 'url_launcher_linux', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux', '', YamlMap.wrap({ 'implements': 'url_launcher', @@ -759,7 +551,7 @@ void main() { appDependencies: directDependencies, ), Plugin.fromYaml( - 'url_launcher_linux_2', + 'user_selected_url_launcher_implementation', '', YamlMap.wrap({ 'implements': 'url_launcher', @@ -774,40 +566,224 @@ void main() { fileSystem: fs, appDependencies: directDependencies, ), - ]); + ], + selectDartPluginsOnly: true, + ); + expect(resolutions.length, equals(1)); + expect(resolutions[0].toMap(), equals( + { + 'pluginName': 'user_selected_url_launcher_implementation', + 'dartClass': 'UrlLauncherPluginLinux', + 'platform': 'linux', + }) + ); + }); + + testWithoutContext('user-selected implementation overrides inline implementation', () async { + final Set directDependencies = { + 'user_selected_url_launcher_implementation', + 'url_launcher', + }; + + final List resolutions = resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'android': { + 'dartPluginClass': 'UrlLauncherAndroid', + }, + 'ios': { + 'dartPluginClass': 'UrlLauncherIos', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'user_selected_url_launcher_implementation', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'android': { + 'dartPluginClass': 'UrlLauncherAndroid', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); + expect(resolutions.length, equals(2)); + expect(resolutions[0].toMap(), equals( + { + 'pluginName': 'user_selected_url_launcher_implementation', + 'dartClass': 'UrlLauncherAndroid', + 'platform': 'android', + }) + ); + expect(resolutions[1].toMap(), equals( + { + 'pluginName': 'url_launcher', + 'dartClass': 'UrlLauncherIos', + 'platform': 'ios', + }) + ); + }); + testUsingContext( + 'provides error when a plugin has a default implementation and implements another plugin', + () async { + final Set directDependencies = { + 'url_launcher', + }; + expect(() { + resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'default_package': 'url_launcher_linux_1', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux_1', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'default_package': 'url_launcher_linux_2', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux_2', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); }, - throwsToolExit( - message: 'Please resolve the plugin implementation selection errors', - )); + throwsToolExit( + message: 'Please resolve the plugin pubspec errors', + )); expect( - testLogger.errorText, - 'Plugin url_launcher:linux has conflicting direct dependency implementations:\n' - ' url_launcher_linux_1\n' - ' url_launcher_linux_2\n' - 'To fix this issue, remove all but one of these dependencies from pubspec.yaml.' - '\n\n' - ); + testLogger.errorText, + 'Plugin url_launcher_linux_1:linux provides an implementation for url_launcher ' + 'and also references a default implementation for url_launcher_linux_2, which is currently not supported. ' + 'Ask the maintainers of url_launcher_linux_1 to either remove the implementation via `implements: url_launcher` ' + 'or avoid referencing a default implementation via `platforms: linux: default_package: url_launcher_linux_2`.' + '\n\n'); }); - testUsingContext('provides all errors when user selected multiple implementations', () async { + testUsingContext( + 'provides error when a plugin has a default implementation and an inline implementation', + () async { final Set directDependencies = { - 'url_launcher_linux_1', - 'url_launcher_linux_2', - 'url_launcher_windows_1', - 'url_launcher_windows_2', + 'url_launcher', }; expect(() { - resolvePlatformImplementation([ + resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ + 'platforms': { + 'linux': { + 'default_package': 'url_launcher_linux', + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); + }, + throwsToolExit( + message: 'Please resolve the plugin pubspec errors', + )); + + expect( + testLogger.errorText, + 'Plugin url_launcher:linux which provides an inline implementation ' + 'cannot also reference a default implementation for url_launcher_linux. ' + 'Ask the maintainers of url_launcher to either remove the implementation via `platforms: linux: dartPluginClass` ' + 'or avoid referencing a default implementation via `platforms: linux: default_package: url_launcher_linux`.' + '\n\n'); + }); + + testUsingContext('provides warning when a plugin references a default plugin without implementation', () async { + final Set directDependencies = {'url_launcher'}; + final List resolutions = + resolvePlatformImplementation( + [ Plugin.fromYaml( - 'url_launcher_linux_1', + 'url_launcher', '', YamlMap.wrap({ - 'implements': 'url_launcher', 'platforms': { 'linux': { - 'dartPluginClass': 'UrlLauncherPluginLinux', + 'default_package': 'url_launcher_linux', }, }, }), @@ -817,13 +793,43 @@ void main() { appDependencies: directDependencies, ), Plugin.fromYaml( - 'url_launcher_linux_2', + 'url_launcher_linux', '', YamlMap.wrap({ 'implements': 'url_launcher', + 'platforms': {}, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); + + expect(resolutions.length, equals(0)); + expect( + testLogger.warningText, + 'Package url_launcher:linux references url_launcher_linux:linux as the default plugin, ' + 'but it does not provide an inline implementation.\n' + 'Ask the maintainers of url_launcher to either avoid referencing a default implementation via `platforms: linux: default_package: url_launcher_linux` ' + 'or add an inline implementation to url_launcher_linux via `platforms: linux:` `pluginClass` or `dartPluginClass`.\n' + '\n'); + }); + + testUsingContext('avoid warning when a plugin references a default plugin with a native implementation only', () async { + final Set directDependencies = {'url_launcher'}; + final List resolutions = + resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher', + '', + YamlMap.wrap({ 'platforms': { 'linux': { - 'dartPluginClass': 'UrlLauncherPluginLinux', + 'default_package': 'url_launcher_linux', }, }, }), @@ -833,13 +839,13 @@ void main() { appDependencies: directDependencies, ), Plugin.fromYaml( - 'url_launcher_windows_1', + 'url_launcher_linux', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { - 'windows': { - 'dartPluginClass': 'UrlLauncherPluginWindows1', + 'linux': { + 'pluginClass': 'UrlLauncherLinux', }, }, }), @@ -848,14 +854,25 @@ void main() { fileSystem: fs, appDependencies: directDependencies, ), + ], + selectDartPluginsOnly: true, + ); + + expect(resolutions.length, equals(0)); + expect(testLogger.warningText, ''); + }); + + testUsingContext('selects default Dart implementation without warning, while choosing plugin selection for nativeOrDart', () async { + final Set directDependencies = {'url_launcher'}; + final List resolutions = resolvePlatformImplementation( + [ Plugin.fromYaml( - 'url_launcher_windows_2', + 'url_launcher', '', YamlMap.wrap({ - 'implements': 'url_launcher', 'platforms': { - 'windows': { - 'dartPluginClass': 'UrlLauncherPluginWindows2', + 'linux': { + 'default_package': 'url_launcher_linux', }, }, }), @@ -864,39 +881,14 @@ void main() { fileSystem: fs, appDependencies: directDependencies, ), - ]); - }, - throwsToolExit( - message: 'Please resolve the plugin implementation selection errors', - )); - - expect( - testLogger.errorText, - 'Plugin url_launcher:linux has conflicting direct dependency implementations:\n' - ' url_launcher_linux_1\n' - ' url_launcher_linux_2\n' - 'To fix this issue, remove all but one of these dependencies from pubspec.yaml.' - '\n\n' - 'Plugin url_launcher:windows has conflicting direct dependency implementations:\n' - ' url_launcher_windows_1\n' - ' url_launcher_windows_2\n' - 'To fix this issue, remove all but one of these dependencies from pubspec.yaml.' - '\n\n' - ); - }); - - testUsingContext('provides error when user needs to select among multiple implementations', () async { - final Set directDependencies = {}; - expect(() { - resolvePlatformImplementation([ Plugin.fromYaml( - 'url_launcher_linux_1', + 'url_launcher_linux', '', YamlMap.wrap({ 'implements': 'url_launcher', 'platforms': { 'linux': { - 'dartPluginClass': 'UrlLauncherPluginLinux1', + 'dartPluginClass': 'UrlLauncherLinux', }, }, }), @@ -905,14 +897,34 @@ void main() { fileSystem: fs, appDependencies: directDependencies, ), + ], + // Using nativeOrDart plugin selection. + selectDartPluginsOnly: false, + ); + expect(resolutions.length, equals(1)); + // Test avoiding trigger a warning for default plugins, while Dart and native plugins selection is enabled. + expect(testLogger.warningText, ''); + expect(resolutions[0].toMap(), equals( + { + 'pluginName': 'url_launcher_linux', + 'dartClass': 'UrlLauncherLinux', + 'platform': 'linux', + }) + ); + }); + + testUsingContext('provides warning when a plugin references a default plugin which does not exist', () async { + final Set directDependencies = {'url_launcher'}; + final List resolutions = + resolvePlatformImplementation( + [ Plugin.fromYaml( - 'url_launcher_linux_2', + 'url_launcher', '', YamlMap.wrap({ - 'implements': 'url_launcher', 'platforms': { 'linux': { - 'dartPluginClass': 'UrlLauncherPluginLinux2', + 'default_package': 'url_launcher_linux', }, }, }), @@ -921,7 +933,215 @@ void main() { fileSystem: fs, appDependencies: directDependencies, ), - ]); + ], + selectDartPluginsOnly: true, + ); + + expect(resolutions.length, equals(0)); + expect( + testLogger.warningText, + 'Package url_launcher:linux references url_launcher_linux:linux as the default plugin, ' + 'but the package does not exist.\n' + 'Ask the maintainers of url_launcher to either avoid referencing a default implementation via `platforms: linux: default_package: url_launcher_linux` ' + 'or create a plugin named url_launcher_linux.\n' + '\n'); + }); + + testUsingContext('provides error when user selected multiple implementations', () async { + 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', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux_2', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); + }, + throwsToolExit( + message: 'Please resolve the plugin implementation selection errors', + )); + + expect( + testLogger.errorText, + 'Plugin url_launcher:linux has conflicting direct dependency implementations:\n' + ' url_launcher_linux_1\n' + ' url_launcher_linux_2\n' + 'To fix this issue, remove all but one of these dependencies from pubspec.yaml.\n' + '\n' + ); + }); + + testUsingContext('provides all errors when user selected multiple implementations', () async { + final Set directDependencies = { + 'url_launcher_linux_1', + 'url_launcher_linux_2', + 'url_launcher_windows_1', + 'url_launcher_windows_2', + }; + expect(() { + resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher_linux_1', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux_2', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_windows_1', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'windows': { + 'dartPluginClass': 'UrlLauncherPluginWindows1', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_windows_2', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'windows': { + 'dartPluginClass': 'UrlLauncherPluginWindows2', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); + }, + throwsToolExit( + message: 'Please resolve the plugin implementation selection errors', + )); + + expect( + testLogger.errorText, + 'Plugin url_launcher:linux has conflicting direct dependency implementations:\n' + ' url_launcher_linux_1\n' + ' url_launcher_linux_2\n' + 'To fix this issue, remove all but one of these dependencies from pubspec.yaml.\n' + '\n' + 'Plugin url_launcher:windows has conflicting direct dependency implementations:\n' + ' url_launcher_windows_1\n' + ' url_launcher_windows_2\n' + 'To fix this issue, remove all but one of these dependencies from pubspec.yaml.\n' + '\n' + ); + }); + + testUsingContext('provides error when user needs to select among multiple implementations', () async { + final Set directDependencies = {}; + expect(() { + resolvePlatformImplementation( + [ + Plugin.fromYaml( + 'url_launcher_linux_1', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux1', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + Plugin.fromYaml( + 'url_launcher_linux_2', + '', + YamlMap.wrap({ + 'implements': 'url_launcher', + 'platforms': { + 'linux': { + 'dartPluginClass': 'UrlLauncherPluginLinux2', + }, + }, + }), + null, + [], + fileSystem: fs, + appDependencies: directDependencies, + ), + ], + selectDartPluginsOnly: true, + ); }, throwsToolExit( message: 'Please resolve the plugin implementation selection errors', @@ -932,8 +1152,8 @@ void main() { 'Plugin url_launcher:linux has multiple possible implementations:\n' ' url_launcher_linux_1\n' ' url_launcher_linux_2\n' - 'To fix this issue, add one of these dependencies to pubspec.yaml.' - '\n\n' + 'To fix this issue, add one of these dependencies to pubspec.yaml.\n' + '\n', ); }); }); diff --git a/packages/flutter_tools/test/general.shard/plugins_test.dart b/packages/flutter_tools/test/general.shard/plugins_test.dart index ecab96583cf..68a45486806 100644 --- a/packages/flutter_tools/test/general.shard/plugins_test.dart +++ b/packages/flutter_tools/test/general.shard/plugins_test.dart @@ -9,6 +9,7 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/base/error_handling_io.dart'; +import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/time.dart'; @@ -892,11 +893,12 @@ dependencies: ProcessManager: () => FakeProcessManager.any(), }); - testUsingContext("Registrant for web doesn't escape slashes in imports", () async { - flutterProject.isModule = true; - final Directory webPluginWithNestedFile = - fs.systemTempDirectory.createTempSync('flutter_web_plugin_with_nested.'); - webPluginWithNestedFile.childFile('pubspec.yaml').writeAsStringSync(''' + group('Build time plugin injection', () { + testUsingContext("Registrant for web doesn't escape slashes in imports", () async { + flutterProject.isModule = true; + final Directory webPluginWithNestedFile = + fs.systemTempDirectory.createTempSync('flutter_web_plugin_with_nested.'); + webPluginWithNestedFile.childFile('pubspec.yaml').writeAsStringSync(''' flutter: plugin: platforms: @@ -904,30 +906,85 @@ dependencies: pluginClass: WebPlugin fileName: src/web_plugin.dart '''); - webPluginWithNestedFile - .childDirectory('lib') - .childDirectory('src') - .childFile('web_plugin.dart') - .createSync(recursive: true); - - flutterProject.directory - .childFile('.packages') - .writeAsStringSync(''' + webPluginWithNestedFile + .childDirectory('lib') + .childDirectory('src') + .childFile('web_plugin.dart') + .createSync(recursive: true); + + flutterProject.directory + .childFile('.packages') + .writeAsStringSync(''' web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri} '''); - final Directory destination = flutterProject.directory.childDirectory('lib'); - await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: destination); + final Directory destination = flutterProject.directory.childDirectory('lib'); + await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: destination); - final File registrant = flutterProject.directory - .childDirectory('lib') - .childFile('web_plugin_registrant.dart'); + final File registrant = flutterProject.directory + .childDirectory('lib') + .childFile('web_plugin_registrant.dart'); - expect(registrant.existsSync(), isTrue); - expect(registrant.readAsStringSync(), contains("import 'package:web_plugin_with_nested/src/web_plugin.dart';")); - }, overrides: { - FileSystem: () => fs, - ProcessManager: () => FakeProcessManager.any(), + expect(registrant.existsSync(), isTrue); + expect(registrant.readAsStringSync(), contains("import 'package:web_plugin_with_nested/src/web_plugin.dart';")); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('user-selected implementation overrides inline implementation on web', () async { + final List directories = createFakePlugins(fs, [ + 'user_selected_url_launcher_implementation', + 'url_launcher', + ]); + + // Add inline web implementation to `user_selected_url_launcher_implementation` + directories[0].childFile('pubspec.yaml').writeAsStringSync(''' +flutter: + plugin: + implements: url_launcher + platforms: + web: + pluginClass: UserSelectedUrlLauncherWeb + fileName: src/web_plugin.dart + '''); + + // Add inline native implementation to `url_launcher` + directories[1].childFile('pubspec.yaml').writeAsStringSync(''' +flutter: + plugin: + platforms: + web: + pluginClass: InlineUrlLauncherWeb + fileName: src/web_plugin.dart + '''); + + final FlutterManifest manifest = FlutterManifest.createFromString(''' +name: test +version: 1.0.0 + +dependencies: + url_launcher: ^1.0.0 + user_selected_url_launcher_implementation: ^1.0.0 + ''', logger: BufferLogger.test())!; + + flutterProject.manifest = manifest; + flutterProject.isModule = true; + + final Directory destination = flutterProject.directory.childDirectory('lib'); + await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: destination); + + final File registrant = flutterProject.directory + .childDirectory('lib') + .childFile('web_plugin_registrant.dart'); + + expect(registrant.existsSync(), isTrue); + expect(registrant.readAsStringSync(), contains("import 'package:user_selected_url_launcher_implementation/src/web_plugin.dart';")); + expect(registrant.readAsStringSync(), isNot(contains("import 'package:url_launcher/src/web_plugin.dart';"))); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }); }); testUsingContext('Injecting creates generated Android registrant, but does not include Dart-only plugins', () async { @@ -1075,6 +1132,112 @@ flutter: ProcessManager: () => FakeProcessManager.any(), }); + testUsingContext('user-selected implementation overrides inline implementation', () async { + final List directories = createFakePlugins(fs, [ + 'user_selected_url_launcher_implementation', + 'url_launcher', + ]); + + // Add inline native implementation to `user_selected_url_launcher_implementation` + directories[0].childFile('pubspec.yaml').writeAsStringSync(''' +flutter: + plugin: + implements: url_launcher + platforms: + linux: + pluginClass: UserSelectedUrlLauncherLinux + '''); + + // Add inline native implementation to `url_launcher` + directories[1].childFile('pubspec.yaml').writeAsStringSync(''' +flutter: + plugin: + platforms: + linux: + pluginClass: InlineUrlLauncherLinux + '''); + + final FlutterManifest manifest = FlutterManifest.createFromString(''' +name: test +version: 1.0.0 + +dependencies: + url_launcher: ^1.0.0 + user_selected_url_launcher_implementation: ^1.0.0 + ''', logger: BufferLogger.test())!; + + flutterProject.manifest = manifest; + + await injectPlugins(flutterProject, linuxPlatform: true); + + final File registrantImpl = linuxProject.managedDirectory.childFile('generated_plugin_registrant.cc'); + + expect(registrantImpl.existsSync(), isTrue); + expect(registrantImpl.readAsStringSync(), contains('user_selected_url_launcher_linux_register_with_registrar')); + expect(registrantImpl.readAsStringSync(), isNot(contains('inline_url_launcher_linux_register_with_registrar'))); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('user-selected implementation overrides default implementation', () async { + final List directories = createFakePlugins(fs, [ + 'user_selected_url_launcher_implementation', + 'url_launcher', + 'url_launcher_linux', + ]); + + // Add inline native implementation to `user_selected_url_launcher_implementation` + directories[0].childFile('pubspec.yaml').writeAsStringSync(''' +flutter: + plugin: + implements: url_launcher + platforms: + linux: + pluginClass: UserSelectedUrlLauncherLinux + '''); + + // Add default native implementation to `url_launcher` + directories[1].childFile('pubspec.yaml').writeAsStringSync(''' +flutter: + plugin: + platforms: + linux: + default_package: url_launcher_linux + '''); + + // Add inline native implementation to `url_launcher_linux` + directories[1].childFile('pubspec.yaml').writeAsStringSync(''' +flutter: + plugin: + platforms: + linux: + pluginClass: InlineUrlLauncherLinux + '''); + + final FlutterManifest manifest = FlutterManifest.createFromString(''' +name: test +version: 1.0.0 + +dependencies: + url_launcher: ^1.0.0 + user_selected_url_launcher_implementation: ^1.0.0 + ''', logger: BufferLogger.test())!; + + flutterProject.manifest = manifest; + + await injectPlugins(flutterProject, linuxPlatform: true); + + final File registrantImpl = linuxProject.managedDirectory.childFile('generated_plugin_registrant.cc'); + + expect(registrantImpl.existsSync(), isTrue); + expect(registrantImpl.readAsStringSync(), contains('user_selected_url_launcher_linux_register_with_registrar')); + expect(registrantImpl.readAsStringSync(), isNot(contains('inline_url_launcher_linux_register_with_registrar'))); + }, overrides: { + FileSystem: () => fs, + ProcessManager: () => FakeProcessManager.any(), + }); + testUsingContext('Injecting creates generated Linux registrant, but does not include Dart-only plugins', () async { // Create a plugin without a pluginClass. final Directory pluginDirectory = createFakePlugin(fs); @@ -1662,6 +1825,7 @@ The Flutter Preview device does not support the following plugins from your pubs expect(plugin.pluginPodspecPath(fs, WindowsPlugin.kConfigKey), isNull); }); }); + testWithoutContext('Symlink failures give developer mode instructions on recent versions of Windows', () async { final Platform platform = FakePlatform(operatingSystem: 'windows'); final FakeOperatingSystemUtils os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14972.1]');