-
Notifications
You must be signed in to change notification settings - Fork 29.7k
feat: Rework getting plugin implementation candidates and plugin resolution #145258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
b5d72e4
refactor: Rework getting plugin implementation candidates and plugin …
Gustl22 8ddfe34
Add docs
Gustl22 12a38f9
docs + refactor: Clarify what _getPluginImplCandidateAndDefaultPlugin…
Gustl22 a190206
make analyzer happy
Gustl22 5b0dfee
Merge remote-tracking branch 'refs/remotes/upstream/main' into 80374-…
Gustl22 4e083b8
Hand over plugin name and candidates
Gustl22 1c0aff6
Split function to _getPluginImplCandidate and _getDefaultPlugin
Gustl22 fbaa6f3
Validate plugin parameters
Gustl22 c316a60
Plugin pubspec validation
Gustl22 751c5d2
Support overriding inline implementation
Gustl22 9fbffbb
Merge remote-tracking branch 'upstream/main' into 80374-refactor-5
Gustl22 2efc0b5
Merge branch 'master' into 80374-refactor-5
Gustl22 c1991f5
Rework error handling
Gustl22 60f7153
cannot
Gustl22 1f9cc09
Blank line before return doc
Gustl22 8c5bac2
Use brackets in => functions
Gustl22 724a0da
Remove comma
Gustl22 f14a896
Make tests happy
Gustl22 d77a0c6
Merge branch 'master' into 80374-refactor-5
Gustl22 5a2657b
Review
Gustl22 685285f
Engaging error message
Gustl22 2023c6a
Merge branch 'master' into 80374-refactor-5
Gustl22 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -97,7 +97,7 @@ Future<List<Plugin>> findPlugins(FlutterProject project, { bool throwOnError = t | |
| package.name, | ||
| packageRoot, | ||
| project.manifest.dependencies, | ||
| fileSystem: fs | ||
| fileSystem: fs, | ||
| ); | ||
| if (plugin != null) { | ||
| plugins.add(plugin); | ||
|
|
@@ -186,9 +186,9 @@ bool _writeFlutterPluginsList( | |
| pluginsMap[platformKey] = _createPluginMapOfPlatform(plugins, platformKey); | ||
| } | ||
|
|
||
| final Map<String, Object> result = <String, Object> {}; | ||
| final Map<String, Object> result = <String, Object>{}; | ||
|
|
||
| result['info'] = 'This is a generated file; do not edit or check into version control.'; | ||
| result['info'] = 'This is a generated file; do not edit or check into version control.'; | ||
| result[_kFlutterPluginsPluginListKey] = pluginsMap; | ||
| /// The dependencyGraph object is kept for backwards compatibility, but | ||
| /// should be removed once migration is complete. | ||
|
|
@@ -1145,7 +1145,7 @@ bool hasPlugins(FlutterProject project) { | |
| // TODO(stuartmorgan): Expand implementation to apply to all implementations, | ||
| // not just Dart-only, per the federated plugin spec. | ||
| List<PluginInterfaceResolution> resolvePlatformImplementation( | ||
| List<Plugin> plugins | ||
| List<Plugin> plugins, | ||
| ) { | ||
| const Iterable<String> platformKeys = <String>[ | ||
| AndroidPlugin.kConfigKey, | ||
|
|
@@ -1154,127 +1154,247 @@ List<PluginInterfaceResolution> resolvePlatformImplementation( | |
| MacOSPlugin.kConfigKey, | ||
| WindowsPlugin.kConfigKey, | ||
| ]; | ||
| final List<PluginInterfaceResolution> finalResolution = <PluginInterfaceResolution>[]; | ||
| final List<PluginInterfaceResolution> pluginResolutions = <PluginInterfaceResolution>[]; | ||
| bool hasResolutionError = false; | ||
| bool hasPluginPubspecError = false; | ||
|
|
||
| for (final String platformKey in platformKeys) { | ||
| // Key: the plugin name | ||
| final Map<String, List<Plugin>> possibleResolutions = <String, List<Plugin>>{}; | ||
| // Key: the plugin name, value: the list of plugin candidates for the implementation of [platformKey]. | ||
| final Map<String, List<Plugin>> pluginImplCandidates = <String, List<Plugin>>{}; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Speaking now of "implementation candidates" as used here already. |
||
|
|
||
| // Key: the plugin name, value: the plugin name of the default implementation of [platformKey]. | ||
| final Map<String, String> defaultImplementations = <String, String>{}; | ||
|
|
||
| for (final Plugin plugin in plugins) { | ||
| final String? defaultImplementation = plugin.defaultPackagePlatforms[platformKey]; | ||
| if (plugin.platforms[platformKey] == null && defaultImplementation == null) { | ||
| // The plugin doesn't implement this platform. | ||
| final String? error = _validatePlugin(plugin, platformKey); | ||
| if (error != null) { | ||
| globals.printError(error); | ||
| hasPluginPubspecError = true; | ||
| continue; | ||
| } | ||
| String? implementsPackage = plugin.implementsPackage; | ||
| if (implementsPackage == null || implementsPackage.isEmpty) { | ||
| final bool hasInlineDartImplementation = | ||
| plugin.pluginDartClassPlatforms[platformKey] != null; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| if (defaultImplementation == null && !hasInlineDartImplementation) { | ||
| // Skip native inline PluginPlatform implementation | ||
| continue; | ||
| } | ||
| if (defaultImplementation != null) { | ||
| defaultImplementations[plugin.name] = defaultImplementation; | ||
| continue; | ||
| } else { | ||
| // An app-facing package (i.e., one with no 'implements') with an | ||
| // inline implementation should be its own default implementation. | ||
| // Desktop platforms originally did not work that way, and enabling | ||
| // it unconditionally would break existing published plugins, so | ||
| // only treat it as such if either: | ||
| // - the platform is not desktop, or | ||
| // - the plugin requires at least Flutter 2.11 (when this opt-in logic | ||
| // was added), so that existing plugins continue to work. | ||
| // See https://github.com/flutter/flutter/issues/87862 for details. | ||
| final bool isDesktop = platformKey == 'linux' || platformKey == 'macos' || platformKey == 'windows'; | ||
| final semver.VersionConstraint? flutterConstraint = plugin.flutterConstraint; | ||
| final semver.Version? minFlutterVersion = flutterConstraint != null && | ||
| flutterConstraint is semver.VersionRange ? flutterConstraint.min : null; | ||
| final bool hasMinVersionForImplementsRequirement = minFlutterVersion != null && | ||
| minFlutterVersion.compareTo(semver.Version(2, 11, 0)) >= 0; | ||
| if (!isDesktop || hasMinVersionForImplementsRequirement) { | ||
| implementsPackage = plugin.name; | ||
| defaultImplementations[plugin.name] = plugin.name; | ||
| } else { | ||
| // If it doesn't meet any of the conditions, it isn't eligible for | ||
| // auto-registration. | ||
| continue; | ||
| } | ||
| } | ||
| final String? implementsPluginName = _getImplementedPlugin(plugin, platformKey); | ||
| final String? defaultImplPluginName = _getDefaultImplPlugin(plugin, platformKey); | ||
|
|
||
| if (defaultImplPluginName != null) { | ||
| // Each plugin can only have one default implementation for this [platformKey]. | ||
| defaultImplementations[plugin.name] = defaultImplPluginName; | ||
| } | ||
| // If there's no Dart implementation, there's nothing to register. | ||
| if (plugin.pluginDartClassPlatforms[platformKey] == null || | ||
| plugin.pluginDartClassPlatforms[platformKey] == 'none') { | ||
| continue; | ||
| if (implementsPluginName != null) { | ||
| pluginImplCandidates.putIfAbsent(implementsPluginName, () => <Plugin>[]); | ||
| pluginImplCandidates[implementsPluginName]!.add(plugin); | ||
| } | ||
|
|
||
| // If it hasn't been skipped, it's a candidate for auto-registration, so | ||
| // add it as a possible resolution. | ||
| possibleResolutions.putIfAbsent(implementsPackage, () => <Plugin>[]); | ||
| possibleResolutions[implementsPackage]!.add(plugin); | ||
| } | ||
|
|
||
| final List<Plugin> pluginResolution = <Plugin>[]; | ||
| final Map<String, Plugin> pluginResolution = <String, Plugin>{}; | ||
|
|
||
| // Resolve all the possible resolutions to a single option for each plugin, or throw if that's not possible. | ||
| for (final MapEntry<String, List<Plugin>> entry in possibleResolutions.entries) { | ||
| final List<Plugin> candidates = entry.value; | ||
| // If there's only one candidate, use it. | ||
| if (candidates.length == 1) { | ||
| pluginResolution.add(candidates.first); | ||
| continue; | ||
| } | ||
| // Next, try direct dependencies of the resolving application. | ||
| final Iterable<Plugin> directDependencies = candidates.where((Plugin plugin) { | ||
| return plugin.isDirectDependency; | ||
| }); | ||
| if (directDependencies.isNotEmpty) { | ||
| if (directDependencies.length > 1) { | ||
| globals.printError( | ||
| 'Plugin ${entry.key}:$platformKey has conflicting direct dependency implementations:\n' | ||
| '${directDependencies.map((Plugin plugin) => ' ${plugin.name}\n').join()}' | ||
| 'To fix this issue, remove all but one of these dependencies from pubspec.yaml.\n' | ||
| ); | ||
| hasResolutionError = true; | ||
| } else { | ||
| pluginResolution.add(directDependencies.first); | ||
| } | ||
| continue; | ||
| } | ||
| // Next, defer to the default implementation if there is one. | ||
| final String? defaultPackageName = defaultImplementations[entry.key]; | ||
| if (defaultPackageName != null) { | ||
| final int defaultIndex = candidates | ||
| .indexWhere((Plugin plugin) => plugin.name == defaultPackageName); | ||
| if (defaultIndex != -1) { | ||
| pluginResolution.add(candidates[defaultIndex]); | ||
| continue; | ||
| } | ||
| } | ||
| // Otherwise, require an explicit choice. | ||
| if (candidates.length > 1) { | ||
| globals.printError( | ||
| 'Plugin ${entry.key}:$platformKey has multiple possible implementations:\n' | ||
| '${candidates.map((Plugin plugin) => ' ${plugin.name}\n').join()}' | ||
| 'To fix this issue, add one of these dependencies to pubspec.yaml.\n' | ||
| ); | ||
| // Now resolve all the possible resolutions to a single option for each | ||
| // plugin, or throw if that's not possible. | ||
| for (final MapEntry<String, List<Plugin>> 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; | ||
| continue; | ||
| } else if (resolution != null) { | ||
| pluginResolution[implCandidatesEntry.key] = resolution; | ||
| } | ||
| } | ||
|
|
||
| finalResolution.addAll( | ||
| pluginResolution.map((Plugin plugin) => | ||
| PluginInterfaceResolution(plugin: plugin, platform: platformKey)), | ||
| pluginResolutions.addAll( | ||
| pluginResolution.values.map((Plugin plugin) { | ||
| return PluginInterfaceResolution(plugin: plugin, platform: platformKey); | ||
| }), | ||
| ); | ||
| } | ||
| if (hasPluginPubspecError) { | ||
| throwToolExit('Please resolve the plugin pubspec errors'); | ||
| } | ||
| if (hasResolutionError) { | ||
| throwToolExit('Please resolve the plugin implementation selection errors'); | ||
| } | ||
| return finalResolution; | ||
| return pluginResolutions; | ||
| } | ||
|
|
||
| /// Validates conflicting plugin parameters in pubspec, such as | ||
| /// `dartPluginClass`, `default_package` and `implements`. | ||
| /// | ||
| /// Returns an error, if failing. | ||
| String? _validatePlugin(Plugin plugin, String platformKey) { | ||
| final String? implementsPackage = plugin.implementsPackage; | ||
| final String? defaultImplPluginName = plugin.defaultPackagePlatforms[platformKey]; | ||
|
|
||
| if (plugin.name == implementsPackage && | ||
| plugin.name == defaultImplPluginName) { | ||
| // Allow self implementing and self as platform default. | ||
| return null; | ||
| } | ||
|
|
||
| if (defaultImplPluginName != null) { | ||
| if (implementsPackage != null && implementsPackage.isNotEmpty) { | ||
| return 'Plugin ${plugin.name}:$platformKey provides an implementation for $implementsPackage ' | ||
| 'and also references a default implementation for $defaultImplPluginName, which is currently not supported. ' | ||
| 'Ask the maintainers of ${plugin.name} to either remove the implementation via `implements: $implementsPackage` ' | ||
| 'or avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName`.\n'; | ||
| } | ||
|
|
||
| if (_hasPluginInlineDartImpl(plugin, platformKey)) { | ||
| 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` ' | ||
| 'or avoid referencing a default implementation via `platforms: $platformKey: default_package: $defaultImplPluginName`.\n'; | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /// Determine if this [plugin] serves as implementation for an app-facing | ||
| /// package for the given platform [platformKey]. | ||
| /// | ||
| /// If so, return the package name, which the [plugin] implements. | ||
Gustl22 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| /// | ||
| /// Options: | ||
| /// * The [plugin] (e.g. 'url_launcher_linux') serves as implementation for | ||
| /// an app-facing package (e.g. 'url_launcher'). | ||
| /// * 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); | ||
|
|
||
| if (hasInlineDartImpl) { | ||
| final String? implementsPackage = plugin.implementsPackage; | ||
|
|
||
| // Only can serve, if the plugin has a dart inline implementation. | ||
| if (implementsPackage != null && implementsPackage.isNotEmpty) { | ||
| return implementsPackage; | ||
| } | ||
|
|
||
| if (_isEligibleDartSelfImpl(plugin, platformKey)) { | ||
| // The inline Dart plugin implements itself. | ||
| return plugin.name; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /// Determine if this [plugin] (or package) references a default plugin with an | ||
| /// implementation for the given platform [platformKey]. | ||
| /// | ||
| /// If so, return the plugin name, which provides the default implementation. | ||
| /// | ||
| /// Options: | ||
| /// * The [plugin] (e.g. 'url_launcher') references a default implementation | ||
| /// (e.g. 'url_launcher_linux'). | ||
| /// * 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) { | ||
| 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. | ||
| return plugin.name; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /// Determine if the [plugin]'s inline dart implementation for the | ||
| /// [platformKey] is eligible to serve as its own default. | ||
| /// | ||
| /// An app-facing package (i.e., one with no 'implements') with an | ||
| /// inline implementation should be its own default implementation. | ||
| /// Desktop platforms originally did not work that way, and enabling | ||
| /// it unconditionally would break existing published plugins, so | ||
| /// only treat it as such if either: | ||
| /// - the platform is not desktop, or | ||
| /// - the plugin requires at least Flutter 2.11 (when this opt-in logic | ||
| /// was added), so that existing plugins continue to work. | ||
| /// See https://github.com/flutter/flutter/issues/87862 for details. | ||
| bool _isEligibleDartSelfImpl(Plugin plugin, String platformKey) { | ||
| final bool isDesktop = platformKey == 'linux' || platformKey == 'macos' || platformKey == 'windows'; | ||
| final semver.VersionConstraint? flutterConstraint = plugin.flutterConstraint; | ||
| final semver.Version? minFlutterVersion = flutterConstraint != null && | ||
| flutterConstraint is semver.VersionRange ? flutterConstraint.min : null; | ||
| final bool hasMinVersionForImplementsRequirement = minFlutterVersion != null && | ||
| minFlutterVersion.compareTo(semver.Version(2, 11, 0)) >= 0; | ||
| return !isDesktop || hasMinVersionForImplementsRequirement; | ||
| } | ||
|
|
||
| /// Determine if the plugin provides an inline dart implementation. | ||
| bool _hasPluginInlineDartImpl(Plugin plugin, String platformKey) { | ||
| return plugin.pluginDartClassPlatforms[platformKey] != null && | ||
| plugin.pluginDartClassPlatforms[platformKey] != 'none'; | ||
| } | ||
|
|
||
| /// Get the resolved plugin [resolution] from the [candidates] serving as implementation for | ||
| /// [pluginName]. | ||
| /// | ||
| /// Returns an [error] string, if failing. | ||
| (Plugin? resolution, String? error) _resolveImplementationOfPlugin({ | ||
| required String platformKey, | ||
| required String pluginName, | ||
| required List<Plugin> candidates, | ||
| String? defaultPackageName, | ||
| }) { | ||
| // If there's only one candidate, use it. | ||
| if (candidates.length == 1) { | ||
| return (candidates.first, null); | ||
| } | ||
| // Next, try direct dependencies of the resolving application. | ||
| final Iterable<Plugin> directDependencies = candidates.where((Plugin plugin) { | ||
| return plugin.isDirectDependency; | ||
| }); | ||
| if (directDependencies.isNotEmpty) { | ||
| if (directDependencies.length > 1) { | ||
Gustl22 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Allow overriding an app-facing package with an inline implementation (which is a direct dependency) | ||
| // with another direct dependency which implements the app-facing package. | ||
| final Iterable<Plugin> implementingPackage = directDependencies.where((Plugin plugin) => plugin.implementsPackage != null && plugin.implementsPackage!.isNotEmpty); | ||
| final Set<Plugin> appFacingPackage = directDependencies.toSet()..removeAll(implementingPackage); | ||
| if (implementingPackage.length == 1 && appFacingPackage.length == 1) { | ||
| return (implementingPackage.first, null); | ||
| } | ||
|
|
||
| return ( | ||
| null, | ||
| 'Plugin $pluginName:$platformKey has conflicting direct dependency implementations:\n' | ||
| '${directDependencies.map((Plugin plugin) => ' ${plugin.name}\n').join()}' | ||
| 'To fix this issue, remove all but one of these dependencies from pubspec.yaml.\n', | ||
| ); | ||
| } else { | ||
| return (directDependencies.first, null); | ||
| } | ||
| } | ||
| // 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); | ||
| } | ||
| } | ||
| // Otherwise, require an explicit choice. | ||
| if (candidates.length > 1) { | ||
| return ( | ||
| null, | ||
| 'Plugin $pluginName:$platformKey has multiple possible implementations:\n' | ||
| '${candidates.map((Plugin plugin) => ' ${plugin.name}\n').join()}' | ||
| 'To fix this issue, add one of these dependencies to pubspec.yaml.\n', | ||
| ); | ||
| } | ||
| // No implementation provided | ||
| return (null, null); | ||
| } | ||
|
|
||
| /// Generates the Dart plugin registrant, which allows to bind a platform | ||
|
|
@@ -1321,7 +1441,7 @@ Future<void> generateMainDartWithPluginRegistrant( | |
| } on FileSystemException catch (error) { | ||
| globals.printWarning( | ||
| 'Unable to remove ${newMainDart.path}, received error: $error.\n' | ||
| 'You might need to run flutter clean.' | ||
| 'You might need to run flutter clean.', | ||
| ); | ||
| rethrow; | ||
| } | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adapted the naming, as "final" is not very descriptive. Instead renaming the other scoped variables should add more clarity.