Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>[];
Copy link
Contributor Author

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.

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>>{};
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here an "inline dart implementation" is defined when if it has no dart plugin class. Here also it is excluded, if it has the none keyword. I suspect the none String basically is never used so it worked, but I unified the variable here. If you have doubts, let me know.

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