Skip to content

Commit

Permalink
feat: Rework getting plugin implementation candidates and plugin reso…
Browse files Browse the repository at this point in the history
…lution (#145258)

Part of #137040 and #80374

- Extracted getting plugin implementation candidates and the default implementation to its own method
- Extracted resolving the plugin implementation to its own method
- Simplify candidate selection algorithm
- Support overriding inline dart implementation for an app-facing plugin
- Throw error, if a federated plugin implements an app-facing plugin, but also references a default implementation
- Throw error, if a plugin provides an inline implementation, but also references a default implementation
  • Loading branch information
Gustl22 authored May 7, 2024
1 parent b8dc9da commit be3e916
Show file tree
Hide file tree
Showing 2 changed files with 408 additions and 104 deletions.
328 changes: 224 additions & 104 deletions packages/flutter_tools/lib/src/flutter_plugins.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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>>{};

// 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;
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.
///
/// 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) {

// 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
Expand Down Expand Up @@ -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;
}
Expand Down
Loading

0 comments on commit be3e916

Please sign in to comment.