feat: Support overriding native endorsed plugins (#137040)

These changes allow to override existing native endorsed (federated & inline) plugins with e.g. non-endorsed plugin implementations via direct dependencies as described in the section *Platform implementation selection* in the [design doc](https://docs.google.com/document/d/1LD7QjmzJZLCopUrFAAE98wOUQpjmguyGTN2wd_89Srs).

```yaml
# pubspec.yaml
name: example
dependencies:
  my_plugin: ^0.0.1
  my_plugin_android_alternative: ^0.0.1
```

Closes #80374, closes #59657

Related: Override Dart plugins (#87991, #79669)
This commit is contained in:
August 2024-07-11 20:54:01 +02:00 committed by GitHub
parent 90025e2964
commit d55a5f96c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 1229 additions and 743 deletions

View File

@ -106,6 +106,12 @@ Future<List<Plugin>> findPlugins(FlutterProject project, { bool throwOnError = t
return plugins; return plugins;
} }
/// Plugin resolution type to determine the injection mechanism.
enum _PluginResolutionType {
dart,
nativeOrDart,
}
// Key strings for the .flutter-plugins-dependencies file. // Key strings for the .flutter-plugins-dependencies file.
const String _kFlutterPluginsPluginListKey = 'plugins'; const String _kFlutterPluginsPluginListKey = 'plugins';
const String _kFlutterPluginsNameKey = 'name'; const String _kFlutterPluginsNameKey = 'name';
@ -181,9 +187,14 @@ bool _writeFlutterPluginsList(
project.web.pluginConfigKey, project.web.pluginConfigKey,
]; ];
final Map<String, List<Plugin>> resolvedPlatformPlugins = _resolvePluginImplementations(
plugins,
pluginResolutionType: _PluginResolutionType.nativeOrDart,
);
final Map<String, Object> pluginsMap = <String, Object>{}; final Map<String, Object> pluginsMap = <String, Object>{};
for (final String platformKey in platformKeys) { for (final String platformKey in platformKeys) {
pluginsMap[platformKey] = _createPluginMapOfPlatform(plugins, platformKey); pluginsMap[platformKey] = _createPluginMapOfPlatform(resolvedPlatformPlugins[platformKey] ?? <Plugin>[], platformKey);
} }
final Map<String, Object> result = <String, Object>{}; final Map<String, Object> result = <String, Object>{};
@ -212,18 +223,15 @@ bool _writeFlutterPluginsList(
} }
/// Creates a map representation of the [plugins] for those supported by [platformKey]. /// Creates a map representation of the [plugins] for those supported by [platformKey].
/// All given [plugins] must provide an implementation for the [platformKey].
List<Map<String, Object>> _createPluginMapOfPlatform( List<Map<String, Object>> _createPluginMapOfPlatform(
List<Plugin> plugins, List<Plugin> plugins,
String platformKey, String platformKey,
) { ) {
final Iterable<Plugin> resolvedPlatformPlugins = plugins.where((Plugin p) { final Set<String> pluginNames = plugins.map((Plugin plugin) => plugin.name).toSet();
return p.platforms.containsKey(platformKey);
});
final Set<String> pluginNames = resolvedPlatformPlugins.map((Plugin plugin) => plugin.name).toSet();
final List<Map<String, Object>> pluginInfo = <Map<String, Object>>[]; final List<Map<String, Object>> pluginInfo = <Map<String, Object>>[];
for (final Plugin plugin in resolvedPlatformPlugins) { for (final Plugin plugin in plugins) {
// This is guaranteed to be non-null due to the `where` filter above. assert(plugin.platforms[platformKey] != null, 'Plugin ${plugin.name} does not provide an implementation for $platformKey.');
final PluginPlatform platformPlugin = plugin.platforms[platformKey]!; final PluginPlatform platformPlugin = plugin.platforms[platformKey]!;
pluginInfo.add(<String, Object>{ pluginInfo.add(<String, Object>{
_kFlutterPluginsNameKey: plugin.name, _kFlutterPluginsNameKey: plugin.name,
@ -1051,10 +1059,9 @@ Future<void> injectBuildTimePluginFiles(
bool webPlatform = false, bool webPlatform = false,
}) async { }) async {
final List<Plugin> plugins = await findPlugins(project); final List<Plugin> plugins = await findPlugins(project);
// Sort the plugins by name to keep ordering stable in generated files. final Map<String, List<Plugin>> pluginsByPlatform = _resolvePluginImplementations(plugins, pluginResolutionType: _PluginResolutionType.nativeOrDart);
plugins.sort((Plugin left, Plugin right) => left.name.compareTo(right.name));
if (webPlatform) { if (webPlatform) {
await _writeWebPluginRegistrant(project, plugins, destination); await _writeWebPluginRegistrant(project, pluginsByPlatform[WebPlugin.kConfigKey]!, destination);
} }
} }
@ -1081,22 +1088,22 @@ Future<void> injectPlugins(
DarwinDependencyManagement? darwinDependencyManagement, DarwinDependencyManagement? darwinDependencyManagement,
}) async { }) async {
final List<Plugin> plugins = await findPlugins(project); final List<Plugin> plugins = await findPlugins(project);
// Sort the plugins by name to keep ordering stable in generated files. final Map<String, List<Plugin>> pluginsByPlatform = _resolvePluginImplementations(plugins, pluginResolutionType: _PluginResolutionType.nativeOrDart);
plugins.sort((Plugin left, Plugin right) => left.name.compareTo(right.name));
if (androidPlatform) { if (androidPlatform) {
await _writeAndroidPluginRegistrant(project, plugins); await _writeAndroidPluginRegistrant(project, pluginsByPlatform[AndroidPlugin.kConfigKey]!);
} }
if (iosPlatform) { if (iosPlatform) {
await _writeIOSPluginRegistrant(project, plugins); await _writeIOSPluginRegistrant(project, pluginsByPlatform[IOSPlugin.kConfigKey]!);
} }
if (linuxPlatform) { if (linuxPlatform) {
await _writeLinuxPluginFiles(project, plugins); await _writeLinuxPluginFiles(project, pluginsByPlatform[LinuxPlugin.kConfigKey]!);
} }
if (macOSPlatform) { if (macOSPlatform) {
await _writeMacOSPluginRegistrant(project, plugins); await _writeMacOSPluginRegistrant(project, pluginsByPlatform[MacOSPlugin.kConfigKey]!);
} }
if (windowsPlatform) { if (windowsPlatform) {
await writeWindowsPluginFiles(project, plugins, globals.templateRenderer, allowedPlugins: allowedPlugins); await writeWindowsPluginFiles(project, pluginsByPlatform[WindowsPlugin.kConfigKey]!, globals.templateRenderer, allowedPlugins: allowedPlugins);
} }
if (iosPlatform || macOSPlatform) { if (iosPlatform || macOSPlatform) {
final DarwinDependencyManagement darwinDependencyManagerSetup = darwinDependencyManagement ?? DarwinDependencyManagement( final DarwinDependencyManagement darwinDependencyManagerSetup = darwinDependencyManagement ?? DarwinDependencyManagement(
@ -1130,7 +1137,7 @@ bool hasPlugins(FlutterProject project) {
return _readFileContent(project.flutterPluginsFile) != null; 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 /// * If there is only one dependency on a package that implements the
/// frontend plugin for the current platform, use that. /// frontend plugin for the current platform, use that.
@ -1142,73 +1149,62 @@ bool hasPlugins(FlutterProject project) {
/// * Else fail. /// * Else fail.
/// ///
/// For more details, https://flutter.dev/go/federated-plugins. /// 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<PluginInterfaceResolution> resolvePlatformImplementation( List<PluginInterfaceResolution> resolvePlatformImplementation(
List<Plugin> plugins, List<Plugin> plugins, {
) { required bool selectDartPluginsOnly,
const Iterable<String> platformKeys = <String>[ }) {
AndroidPlugin.kConfigKey, final Map<String, List<Plugin>> resolution = _resolvePluginImplementations(
IOSPlugin.kConfigKey, plugins,
LinuxPlugin.kConfigKey, pluginResolutionType: selectDartPluginsOnly ? _PluginResolutionType.dart : _PluginResolutionType.nativeOrDart,
MacOSPlugin.kConfigKey, );
WindowsPlugin.kConfigKey, return resolution.entries.expand((MapEntry<String, List<Plugin>> entry) {
]; return entry.value.map((Plugin plugin) {
final List<PluginInterfaceResolution> pluginResolutions = <PluginInterfaceResolution>[]; return PluginInterfaceResolution(plugin: plugin, platform: entry.key);
bool hasResolutionError = false; });
}).toList();
}
/// Resolves the plugin implementations for all platforms,
/// see [resolvePlatformImplementation].
///
/// Only plugins which provide the according platform implementation are returned.
Map<String, List<Plugin>> _resolvePluginImplementations(
List<Plugin> plugins, {
required _PluginResolutionType pluginResolutionType,
}) {
final Map<String, List<Plugin>> pluginsByPlatform = <String, List<Plugin>>{
AndroidPlugin.kConfigKey: <Plugin>[],
IOSPlugin.kConfigKey: <Plugin>[],
LinuxPlugin.kConfigKey: <Plugin>[],
MacOSPlugin.kConfigKey: <Plugin>[],
WindowsPlugin.kConfigKey: <Plugin>[],
WebPlugin.kConfigKey: <Plugin>[],
};
bool hasPluginPubspecError = false; bool hasPluginPubspecError = false;
bool hasResolutionError = false;
for (final String platformKey in platformKeys) { for (final String platformKey in pluginsByPlatform.keys) {
// Key: the plugin name, value: the list of plugin candidates for the implementation of [platformKey]. final (
final Map<String, List<Plugin>> pluginImplCandidates = <String, List<Plugin>>{}; List<Plugin> platformPluginResolutions,
bool hasPlatformPluginPubspecError,
// Key: the plugin name, value: the plugin name of the default implementation of [platformKey]. bool hasPlatformResolutionError
final Map<String, String> defaultImplementations = <String, String>{}; ) = _resolvePluginImplementationsByPlatform(
plugins,
for (final Plugin plugin in plugins) { platformKey,
final String? error = _validatePlugin(plugin, platformKey); pluginResolutionType: pluginResolutionType,
if (error != null) {
globals.printError(error);
hasPluginPubspecError = true;
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 (implementsPluginName != null) {
pluginImplCandidates.putIfAbsent(implementsPluginName, () => <Plugin>[]);
pluginImplCandidates[implementsPluginName]!.add(plugin);
}
}
final Map<String, Plugin> pluginResolution = <String, Plugin>{};
// 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;
} else if (resolution != null) {
pluginResolution[implCandidatesEntry.key] = resolution;
}
}
pluginResolutions.addAll(
pluginResolution.values.map((Plugin plugin) {
return PluginInterfaceResolution(plugin: plugin, platform: platformKey);
}),
); );
if (hasPlatformPluginPubspecError) {
hasPluginPubspecError = true;
} else if (hasPlatformResolutionError) {
hasResolutionError = true;
} else {
pluginsByPlatform[platformKey] = platformPluginResolutions;
}
} }
if (hasPluginPubspecError) { if (hasPluginPubspecError) {
throwToolExit('Please resolve the plugin pubspec errors'); throwToolExit('Please resolve the plugin pubspec errors');
@ -1216,14 +1212,103 @@ List<PluginInterfaceResolution> resolvePlatformImplementation(
if (hasResolutionError) { if (hasResolutionError) {
throwToolExit('Please resolve the plugin implementation selection errors'); 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<Plugin> pluginImplementations, bool hasPluginPubspecError, bool hasResolutionError) _resolvePluginImplementationsByPlatform(
Iterable<Plugin> 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<String, List<Plugin>> pluginImplCandidates = <String, List<Plugin>>{};
// Key: the plugin name, value: the plugin of the default implementation of [platformKey].
final Map<String, Plugin> defaultImplementations = <String, Plugin>{};
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, () => <Plugin>[]);
pluginImplCandidates[implementsPluginName]!.add(plugin);
}
}
// Key: the plugin name, value: the plugin which provides an implementation for [platformKey].
final Map<String, Plugin> pluginResolution = <String, Plugin>{};
// 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,
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<Plugin> 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 /// Validates conflicting plugin parameters in pubspec, such as
/// `dartPluginClass`, `default_package` and `implements`. /// `dartPluginClass`, `default_package` and `implements`.
/// ///
/// Returns an error, if failing. /// 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? implementsPackage = plugin.implementsPackage;
final String? defaultImplPluginName = plugin.defaultPackagePlatforms[platformKey]; 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'; '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 ' return 'Plugin ${plugin.name}:$platformKey which provides an inline implementation '
'cannot also reference a default implementation for $defaultImplPluginName. ' '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'; '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 /// * The [plugin] (e.g. 'url_launcher') implements itself and then also
/// serves as its own default implementation. /// serves as its own default implementation.
/// * The [plugin] does not provide an implementation. /// * The [plugin] does not provide an implementation.
String? _getImplementedPlugin(Plugin plugin, String platformKey) { String? _getImplementedPlugin(
final bool hasInlineDartImpl = _hasPluginInlineDartImpl(plugin, platformKey); 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; final String? implementsPackage = plugin.implementsPackage;
// Only can serve, if the plugin has a dart inline implementation.
if (implementsPackage != null && implementsPackage.isNotEmpty) { if (implementsPackage != null && implementsPackage.isNotEmpty) {
// The inline plugin implements another package.
return implementsPackage; return implementsPackage;
} }
if (_isEligibleDartSelfImpl(plugin, platformKey)) { if (pluginResolutionType == _PluginResolutionType.nativeOrDart || _isEligibleDartSelfImpl(plugin, platformKey)) {
// The inline Dart plugin implements itself. // The inline plugin implements itself.
return plugin.name; return plugin.name;
} }
} }
@ -1293,16 +1381,20 @@ String? _getImplementedPlugin(Plugin plugin, String platformKey) {
/// * The [plugin] (e.g. 'url_launcher') implements itself and then also /// * The [plugin] (e.g. 'url_launcher') implements itself and then also
/// serves as its own default implementation. /// serves as its own default implementation.
/// * The [plugin] does not reference a 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 = final String? defaultImplPluginName =
plugin.defaultPackagePlatforms[platformKey]; plugin.defaultPackagePlatforms[platformKey];
if (defaultImplPluginName != null) { if (defaultImplPluginName != null) {
return defaultImplPluginName; return defaultImplPluginName;
} }
if (_hasPluginInlineDartImpl(plugin, platformKey) && if (_hasPluginInlineImpl(plugin, platformKey, pluginResolutionType: pluginResolutionType) &&
_isEligibleDartSelfImpl(plugin, platformKey)) { (pluginResolutionType == _PluginResolutionType.nativeOrDart || _isEligibleDartSelfImpl(plugin, platformKey))) {
// The inline Dart plugin serves as its own default implementation. // The inline plugin serves as its own default implementation.
return plugin.name; return plugin.name;
} }
@ -1331,7 +1423,17 @@ bool _isEligibleDartSelfImpl(Plugin plugin, String platformKey) {
return !isDesktop || hasMinVersionForImplementsRequirement; 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) { bool _hasPluginInlineDartImpl(Plugin plugin, String platformKey) {
return plugin.pluginDartClassPlatforms[platformKey] != null && return plugin.pluginDartClassPlatforms[platformKey] != null &&
plugin.pluginDartClassPlatforms[platformKey] != 'none'; plugin.pluginDartClassPlatforms[platformKey] != 'none';
@ -1343,9 +1445,10 @@ bool _hasPluginInlineDartImpl(Plugin plugin, String platformKey) {
/// Returns an [error] string, if failing. /// Returns an [error] string, if failing.
(Plugin? resolution, String? error) _resolveImplementationOfPlugin({ (Plugin? resolution, String? error) _resolveImplementationOfPlugin({
required String platformKey, required String platformKey,
required _PluginResolutionType pluginResolutionType,
required String pluginName, required String pluginName,
required List<Plugin> candidates, required List<Plugin> candidates,
String? defaultPackageName, Plugin? defaultPackage,
}) { }) {
// If there's only one candidate, use it. // If there's only one candidate, use it.
if (candidates.length == 1) { if (candidates.length == 1) {
@ -1377,12 +1480,10 @@ bool _hasPluginInlineDartImpl(Plugin plugin, String platformKey) {
} }
} }
// Next, defer to the default implementation if there is one. // Next, defer to the default implementation if there is one.
if (defaultPackageName != null) { if (defaultPackage != null && candidates.contains(defaultPackage)) {
final int defaultIndex = candidates // By definition every candidate has an inline implementation
.indexWhere((Plugin plugin) => plugin.name == defaultPackageName); assert(_hasPluginInlineImpl(defaultPackage, platformKey, pluginResolutionType: pluginResolutionType));
if (defaultIndex != -1) { return (defaultPackage, null);
return (candidates[defaultIndex], null);
}
} }
// Otherwise, require an explicit choice. // Otherwise, require an explicit choice.
if (candidates.length > 1) { if (candidates.length > 1) {
@ -1417,6 +1518,7 @@ Future<void> generateMainDartWithPluginRegistrant(
final List<Plugin> plugins = await findPlugins(rootProject); final List<Plugin> plugins = await findPlugins(rootProject);
final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation( final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(
plugins, plugins,
selectDartPluginsOnly: true,
); );
final LanguageVersion entrypointVersion = determineLanguageVersion( final LanguageVersion entrypointVersion = determineLanguageVersion(
mainFile, mainFile,

View File

@ -142,8 +142,8 @@ class Plugin {
} }
// TODO(stuartmorgan): Consider merging web into this common handling; the // TODO(stuartmorgan): Consider merging web into this common handling; the
// fact that its implementation of Dart-only plugins and default packages // fact that its implementation of Dart-only plugins and default packages
// are separate is legacy. // are separate is legacy.
final List<String> sharedHandlingPlatforms = <String>[ final List<String> sharedHandlingPlatforms = <String>[
AndroidPlugin.kConfigKey, AndroidPlugin.kConfigKey,
IOSPlugin.kConfigKey, IOSPlugin.kConfigKey,
@ -382,7 +382,7 @@ class Plugin {
/// This is a mapping from platform config key to the default package implementation. /// This is a mapping from platform config key to the default package implementation.
final Map<String, String> defaultPackagePlatforms; final Map<String, String> 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<String, String> pluginDartClassPlatforms; final Map<String, String> pluginDartClassPlatforms;
/// Whether this plugin is a direct dependency of the app. /// Whether this plugin is a direct dependency of the app.

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ import 'package:file/file.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart'; import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/error_handling_io.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/os.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/time.dart'; import 'package:flutter_tools/src/base/time.dart';
@ -892,11 +893,12 @@ dependencies:
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext("Registrant for web doesn't escape slashes in imports", () async { group('Build time plugin injection', () {
flutterProject.isModule = true; testUsingContext("Registrant for web doesn't escape slashes in imports", () async {
final Directory webPluginWithNestedFile = flutterProject.isModule = true;
fs.systemTempDirectory.createTempSync('flutter_web_plugin_with_nested.'); final Directory webPluginWithNestedFile =
webPluginWithNestedFile.childFile('pubspec.yaml').writeAsStringSync(''' fs.systemTempDirectory.createTempSync('flutter_web_plugin_with_nested.');
webPluginWithNestedFile.childFile('pubspec.yaml').writeAsStringSync('''
flutter: flutter:
plugin: plugin:
platforms: platforms:
@ -904,30 +906,85 @@ dependencies:
pluginClass: WebPlugin pluginClass: WebPlugin
fileName: src/web_plugin.dart fileName: src/web_plugin.dart
'''); ''');
webPluginWithNestedFile webPluginWithNestedFile
.childDirectory('lib') .childDirectory('lib')
.childDirectory('src') .childDirectory('src')
.childFile('web_plugin.dart') .childFile('web_plugin.dart')
.createSync(recursive: true); .createSync(recursive: true);
flutterProject.directory flutterProject.directory
.childFile('.packages') .childFile('.packages')
.writeAsStringSync(''' .writeAsStringSync('''
web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri} web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri}
'''); ''');
final Directory destination = flutterProject.directory.childDirectory('lib'); final Directory destination = flutterProject.directory.childDirectory('lib');
await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: destination); await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: destination);
final File registrant = flutterProject.directory final File registrant = flutterProject.directory
.childDirectory('lib') .childDirectory('lib')
.childFile('web_plugin_registrant.dart'); .childFile('web_plugin_registrant.dart');
expect(registrant.existsSync(), isTrue); expect(registrant.existsSync(), isTrue);
expect(registrant.readAsStringSync(), contains("import 'package:web_plugin_with_nested/src/web_plugin.dart';")); expect(registrant.readAsStringSync(), contains("import 'package:web_plugin_with_nested/src/web_plugin.dart';"));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
FileSystem: () => fs, FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('user-selected implementation overrides inline implementation on web', () async {
final List<Directory> directories = createFakePlugins(fs, <String>[
'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: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
}); });
testUsingContext('Injecting creates generated Android registrant, but does not include Dart-only plugins', () async { testUsingContext('Injecting creates generated Android registrant, but does not include Dart-only plugins', () async {
@ -1075,6 +1132,112 @@ flutter:
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('user-selected implementation overrides inline implementation', () async {
final List<Directory> directories = createFakePlugins(fs, <String>[
'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: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('user-selected implementation overrides default implementation', () async {
final List<Directory> directories = createFakePlugins(fs, <String>[
'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: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('Injecting creates generated Linux registrant, but does not include Dart-only plugins', () async { testUsingContext('Injecting creates generated Linux registrant, but does not include Dart-only plugins', () async {
// Create a plugin without a pluginClass. // Create a plugin without a pluginClass.
final Directory pluginDirectory = createFakePlugin(fs); 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); expect(plugin.pluginPodspecPath(fs, WindowsPlugin.kConfigKey), isNull);
}); });
}); });
testWithoutContext('Symlink failures give developer mode instructions on recent versions of Windows', () async { testWithoutContext('Symlink failures give developer mode instructions on recent versions of Windows', () async {
final Platform platform = FakePlatform(operatingSystem: 'windows'); final Platform platform = FakePlatform(operatingSystem: 'windows');
final FakeOperatingSystemUtils os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14972.1]'); final FakeOperatingSystemUtils os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14972.1]');