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

View File

@ -142,8 +142,8 @@ class Plugin {
}
// TODO(stuartmorgan): Consider merging web into this common handling; the
// fact that its implementation of Dart-only plugins and default packages
// are separate is legacy.
// fact that its implementation of Dart-only plugins and default packages
// are separate is legacy.
final List<String> sharedHandlingPlatforms = <String>[
AndroidPlugin.kConfigKey,
IOSPlugin.kConfigKey,
@ -382,7 +382,7 @@ class Plugin {
/// This is a mapping from platform config key to the default package implementation.
final Map<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;
/// 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_testing/file_testing.dart';
import 'package:flutter_tools/src/base/error_handling_io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/time.dart';
@ -892,11 +893,12 @@ dependencies:
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext("Registrant for web doesn't escape slashes in imports", () async {
flutterProject.isModule = true;
final Directory webPluginWithNestedFile =
fs.systemTempDirectory.createTempSync('flutter_web_plugin_with_nested.');
webPluginWithNestedFile.childFile('pubspec.yaml').writeAsStringSync('''
group('Build time plugin injection', () {
testUsingContext("Registrant for web doesn't escape slashes in imports", () async {
flutterProject.isModule = true;
final Directory webPluginWithNestedFile =
fs.systemTempDirectory.createTempSync('flutter_web_plugin_with_nested.');
webPluginWithNestedFile.childFile('pubspec.yaml').writeAsStringSync('''
flutter:
plugin:
platforms:
@ -904,30 +906,85 @@ dependencies:
pluginClass: WebPlugin
fileName: src/web_plugin.dart
''');
webPluginWithNestedFile
.childDirectory('lib')
.childDirectory('src')
.childFile('web_plugin.dart')
.createSync(recursive: true);
webPluginWithNestedFile
.childDirectory('lib')
.childDirectory('src')
.childFile('web_plugin.dart')
.createSync(recursive: true);
flutterProject.directory
.childFile('.packages')
.writeAsStringSync('''
flutterProject.directory
.childFile('.packages')
.writeAsStringSync('''
web_plugin_with_nested:${webPluginWithNestedFile.childDirectory('lib').uri}
''');
final Directory destination = flutterProject.directory.childDirectory('lib');
await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: destination);
final Directory destination = flutterProject.directory.childDirectory('lib');
await injectBuildTimePluginFiles(flutterProject, webPlatform: true, destination: destination);
final File registrant = flutterProject.directory
.childDirectory('lib')
.childFile('web_plugin_registrant.dart');
final File registrant = flutterProject.directory
.childDirectory('lib')
.childFile('web_plugin_registrant.dart');
expect(registrant.existsSync(), isTrue);
expect(registrant.readAsStringSync(), contains("import 'package:web_plugin_with_nested/src/web_plugin.dart';"));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
ProcessManager: () => FakeProcessManager.any(),
expect(registrant.existsSync(), isTrue);
expect(registrant.readAsStringSync(), contains("import 'package:web_plugin_with_nested/src/web_plugin.dart';"));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
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 {
@ -1075,6 +1132,112 @@ flutter:
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 {
// Create a plugin without a pluginClass.
final Directory pluginDirectory = createFakePlugin(fs);
@ -1662,6 +1825,7 @@ The Flutter Preview device does not support the following plugins from your pubs
expect(plugin.pluginPodspecPath(fs, WindowsPlugin.kConfigKey), isNull);
});
});
testWithoutContext('Symlink failures give developer mode instructions on recent versions of Windows', () async {
final Platform platform = FakePlatform(operatingSystem: 'windows');
final FakeOperatingSystemUtils os = FakeOperatingSystemUtils('Microsoft Windows [Version 10.0.14972.1]');