Unverified Commit 62116f99 authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Improve Dart plugin registration handling (#122046)

Improve Dart plugin registration handling
parent 7e000b29
...@@ -1191,13 +1191,13 @@ bool hasPlugins(FlutterProject project) { ...@@ -1191,13 +1191,13 @@ bool hasPlugins(FlutterProject project) {
/// Resolves the platform implementation for Dart-only plugins. /// Resolves the platform implementation for Dart-only plugins.
/// ///
/// * If there are multiple direct pub dependencies on packages that implement the /// * If there is only one dependency on a package that implements the
/// frontend plugin for the current platform, fail. /// frontend plugin for the current platform, use that.
/// * If there is a single direct dependency on a package that implements the /// * If there is a single direct dependency on a package that implements the
/// frontend plugin for the target platform, this package is the selected implementation. /// frontend plugin for the current platform, use that.
/// * If there is no direct dependency on a package that implements the frontend /// * If there is no direct dependency on a package that implements the
/// plugin for the target platform, and the frontend plugin has a default implementation /// frontend plugin, but there is a default for the current platform,
/// for the target platform the default implementation is selected. /// use that.
/// * Else fail. /// * Else fail.
/// ///
/// For more details, https://flutter.dev/go/federated-plugins. /// For more details, https://flutter.dev/go/federated-plugins.
...@@ -1214,11 +1214,15 @@ List<PluginInterfaceResolution> resolvePlatformImplementation( ...@@ -1214,11 +1214,15 @@ List<PluginInterfaceResolution> resolvePlatformImplementation(
MacOSPlugin.kConfigKey, MacOSPlugin.kConfigKey,
WindowsPlugin.kConfigKey, WindowsPlugin.kConfigKey,
]; ];
final Map<String, PluginInterfaceResolution> directDependencyResolutions final Map<String, List<PluginInterfaceResolution>> possibleResolutions
= <String, PluginInterfaceResolution>{}; = <String, List<PluginInterfaceResolution>>{};
final Map<String, String> defaultImplementations = <String, String>{}; final Map<String, String> defaultImplementations = <String, String>{};
bool didFindError = false; // Generates a key for the maps above.
String getResolutionKey({required String platform, required String packageName}) {
return '$packageName:$platform';
}
bool hasPubspecError = false;
for (final Plugin plugin in plugins) { for (final Plugin plugin in plugins) {
for (final String platform in platforms) { for (final String platform in platforms) {
if (plugin.platforms[platform] == null && if (plugin.platforms[platform] == null &&
...@@ -1257,11 +1261,12 @@ List<PluginInterfaceResolution> resolvePlatformImplementation( ...@@ -1257,11 +1261,12 @@ List<PluginInterfaceResolution> resolvePlatformImplementation(
'\n' '\n'
); );
} }
didFindError = true; hasPubspecError = true;
continue; continue;
} }
final String defaultImplementationKey = getResolutionKey(platform: platform, packageName: plugin.name);
if (defaultImplementation != null) { if (defaultImplementation != null) {
defaultImplementations['$platform/${plugin.name}'] = defaultImplementation; defaultImplementations[defaultImplementationKey] = defaultImplementation;
continue; continue;
} else { } else {
// An app-facing package (i.e., one with no 'implements') with an // An app-facing package (i.e., one with no 'implements') with an
...@@ -1281,52 +1286,87 @@ List<PluginInterfaceResolution> resolvePlatformImplementation( ...@@ -1281,52 +1286,87 @@ List<PluginInterfaceResolution> resolvePlatformImplementation(
minFlutterVersion.compareTo(semver.Version(2, 11, 0)) >= 0; minFlutterVersion.compareTo(semver.Version(2, 11, 0)) >= 0;
if (!isDesktop || hasMinVersionForImplementsRequirement) { if (!isDesktop || hasMinVersionForImplementsRequirement) {
implementsPackage = plugin.name; implementsPackage = plugin.name;
defaultImplementations['$platform/${plugin.name}'] = plugin.name; defaultImplementations[defaultImplementationKey] = plugin.name;
} else {
// If it doesn't meet any of the conditions, it isn't eligible for
// auto-registration.
continue;
} }
} }
} }
// If there's no Dart implementation, there's nothing to register.
if (plugin.pluginDartClassPlatforms[platform] == null || if (plugin.pluginDartClassPlatforms[platform] == null ||
plugin.pluginDartClassPlatforms[platform] == 'none') { plugin.pluginDartClassPlatforms[platform] == 'none') {
continue; continue;
} }
final String resolutionKey = '$platform/$implementsPackage';
if (directDependencyResolutions.containsKey(resolutionKey)) { // If it hasn't been skipped, it's a candidate for auto-registration, so
final PluginInterfaceResolution? currResolution = directDependencyResolutions[resolutionKey]; // add it as a possible resolution.
if (currResolution != null && currResolution.plugin.isDirectDependency) { final String resolutionKey = getResolutionKey(platform: platform, packageName: implementsPackage);
if (plugin.isDirectDependency) { if (!possibleResolutions.containsKey(resolutionKey)) {
if (throwOnPluginPubspecError) { possibleResolutions[resolutionKey] = <PluginInterfaceResolution>[];
globals.printError(
'Plugin `${plugin.name}` implements an interface for `$platform`, which was already '
'implemented by plugin `${currResolution.plugin.name}`.\n'
'To fix this issue, remove either dependency from pubspec.yaml.'
'\n\n'
);
}
didFindError = true;
}
// Use the plugin implementation added by the user as a direct dependency.
continue;
}
} }
directDependencyResolutions[resolutionKey] = PluginInterfaceResolution( possibleResolutions[resolutionKey]!.add(PluginInterfaceResolution(
plugin: plugin, plugin: plugin,
platform: platform, platform: platform,
); ));
} }
} }
if (didFindError && throwOnPluginPubspecError) { if (hasPubspecError && throwOnPluginPubspecError) {
throwToolExit('Please resolve the errors'); throwToolExit('Please resolve the errors');
} }
// Now resolve all the possible resolutions to a single option for each
// plugin, or throw if that's not possible.
bool hasResolutionError = false;
final List<PluginInterfaceResolution> finalResolution = <PluginInterfaceResolution>[]; final List<PluginInterfaceResolution> finalResolution = <PluginInterfaceResolution>[];
for (final MapEntry<String, PluginInterfaceResolution> resolution in directDependencyResolutions.entries) { for (final MapEntry<String, List<PluginInterfaceResolution>> entry in possibleResolutions.entries) {
if (resolution.value.plugin.isDirectDependency) { final List<PluginInterfaceResolution> candidates = entry.value;
finalResolution.add(resolution.value); // If there's only one candidate, use it.
} else if (defaultImplementations.containsKey(resolution.key)) { if (candidates.length == 1) {
// Pick the default implementation. finalResolution.add(candidates.first);
if (defaultImplementations[resolution.key] == resolution.value.plugin.name) { continue;
finalResolution.add(resolution.value); }
// Next, try direct dependencies of the resolving application.
final Iterable<PluginInterfaceResolution> directDependencies = candidates.where((PluginInterfaceResolution r) {
return r.plugin.isDirectDependency;
});
if (directDependencies.isNotEmpty) {
if (directDependencies.length > 1) {
globals.printError(
'Plugin ${entry.key} has conflicting direct dependency implementations:\n'
'${directDependencies.map((PluginInterfaceResolution r) => ' ${r.plugin.name}\n').join()}'
'To fix this issue, remove all but one of these dependencies from pubspec.yaml.\n'
);
hasResolutionError = true;
} else {
finalResolution.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((PluginInterfaceResolution r) => r.plugin.name == defaultPackageName);
if (defaultIndex != -1) {
finalResolution.add(candidates[defaultIndex]);
continue;
} }
} }
// Otherwise, require an explicit choice.
if (candidates.length > 1) {
globals.printError(
'Plugin ${entry.key} has multiple possible implementations:\n'
'${candidates.map((PluginInterfaceResolution r) => ' ${r.plugin.name}\n').join()}'
'To fix this issue, add one of these dependencies to pubspec.yaml.\n'
);
hasResolutionError = true;
continue;
}
}
if (hasResolutionError) {
throwToolExit('Please resolve the errors');
} }
return finalResolution; return finalResolution;
} }
......
...@@ -418,4 +418,9 @@ class PluginInterfaceResolution { ...@@ -418,4 +418,9 @@ class PluginInterfaceResolution {
'dartClass': plugin.pluginDartClassPlatforms[platform] ?? '', 'dartClass': plugin.pluginDartClassPlatforms[platform] ?? '',
}; };
} }
@override
String toString() {
return '<PluginInterfaceResolution ${plugin.name} for $platform>';
}
} }
...@@ -38,7 +38,7 @@ void main() { ...@@ -38,7 +38,7 @@ void main() {
}); });
group('resolvePlatformImplementation', () { group('resolvePlatformImplementation', () {
testWithoutContext('selects implementation from direct dependency', () async { testWithoutContext('selects uncontested implementation from direct dependency', () async {
final Set<String> directDependencies = <String>{ final Set<String> directDependencies = <String>{
'url_launcher_linux', 'url_launcher_linux',
'url_launcher_macos', 'url_launcher_macos',
...@@ -76,14 +76,38 @@ void main() { ...@@ -76,14 +76,38 @@ void main() {
fileSystem: fs, fileSystem: fs,
appDependencies: directDependencies, appDependencies: directDependencies,
), ),
]);
expect(resolutions.length, equals(2));
expect(resolutions[0].toMap(), equals(
<String, String>{
'pluginName': 'url_launcher_linux',
'dartClass': 'UrlLauncherPluginLinux',
'platform': 'linux',
})
);
expect(resolutions[1].toMap(), equals(
<String, String>{
'pluginName': 'url_launcher_macos',
'dartClass': 'UrlLauncherPluginMacOS',
'platform': 'macos',
})
);
});
testWithoutContext('selects uncontested implementation from transitive dependency', () async {
final Set<String> directDependencies = <String>{
'url_launcher_macos',
};
final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(<Plugin>[
Plugin.fromYaml( Plugin.fromYaml(
'undirect_dependency_plugin', 'url_launcher_macos',
'', '',
YamlMap.wrap(<String, dynamic>{ YamlMap.wrap(<String, dynamic>{
'implements': 'url_launcher', 'implements': 'url_launcher',
'platforms': <String, dynamic>{ 'platforms': <String, dynamic>{
'windows': <String, dynamic>{ 'macos': <String, dynamic>{
'dartPluginClass': 'UrlLauncherPluginWindows', 'dartPluginClass': 'UrlLauncherPluginMacOS',
}, },
}, },
}), }),
...@@ -92,17 +116,14 @@ void main() { ...@@ -92,17 +116,14 @@ void main() {
fileSystem: fs, fileSystem: fs,
appDependencies: directDependencies, appDependencies: directDependencies,
), ),
]);
resolvePlatformImplementation(<Plugin>[
Plugin.fromYaml( Plugin.fromYaml(
'url_launcher_macos', 'transitive_dependency_plugin',
'', '',
YamlMap.wrap(<String, dynamic>{ YamlMap.wrap(<String, dynamic>{
'implements': 'url_launcher', 'implements': 'url_launcher',
'platforms': <String, dynamic>{ 'platforms': <String, dynamic>{
'macos': <String, dynamic>{ 'windows': <String, dynamic>{
'dartPluginClass': 'UrlLauncherPluginMacOS', 'dartPluginClass': 'UrlLauncherPluginWindows',
}, },
}, },
}), }),
...@@ -116,16 +137,16 @@ void main() { ...@@ -116,16 +137,16 @@ void main() {
expect(resolutions.length, equals(2)); expect(resolutions.length, equals(2));
expect(resolutions[0].toMap(), equals( expect(resolutions[0].toMap(), equals(
<String, String>{ <String, String>{
'pluginName': 'url_launcher_linux', 'pluginName': 'url_launcher_macos',
'dartClass': 'UrlLauncherPluginLinux', 'dartClass': 'UrlLauncherPluginMacOS',
'platform': 'linux', 'platform': 'macos',
}) })
); );
expect(resolutions[1].toMap(), equals( expect(resolutions[1].toMap(), equals(
<String, String>{ <String, String>{
'pluginName': 'url_launcher_macos', 'pluginName': 'transitive_dependency_plugin',
'dartClass': 'UrlLauncherPluginMacOS', 'dartClass': 'UrlLauncherPluginWindows',
'platform': 'macos', 'platform': 'windows',
}) })
); );
}); });
...@@ -301,14 +322,17 @@ void main() { ...@@ -301,14 +322,17 @@ void main() {
fileSystem: fs, fileSystem: fs,
appDependencies: directDependencies, appDependencies: directDependencies,
), ),
// Include three possible implementations, one before and one after
// to ensure that the selection is working as intended, not just by
// coincidence of order.
Plugin.fromYaml( Plugin.fromYaml(
'url_launcher_linux', 'another_url_launcher_linux',
'', '',
YamlMap.wrap(<String, dynamic>{ YamlMap.wrap(<String, dynamic>{
'implements': 'url_launcher', 'implements': 'url_launcher',
'platforms': <String, dynamic>{ 'platforms': <String, dynamic>{
'linux': <String, dynamic>{ 'linux': <String, dynamic>{
'dartPluginClass': 'UrlLauncherPluginLinux', 'dartPluginClass': 'UnofficialUrlLauncherPluginLinux',
}, },
}, },
}), }),
...@@ -317,28 +341,14 @@ void main() { ...@@ -317,28 +341,14 @@ void main() {
fileSystem: fs, fileSystem: fs,
appDependencies: directDependencies, appDependencies: directDependencies,
), ),
]);
expect(resolutions.length, equals(1));
expect(resolutions[0].toMap(), equals(
<String, String>{
'pluginName': 'url_launcher_linux',
'dartClass': 'UrlLauncherPluginLinux',
'platform': 'linux',
})
);
});
testWithoutContext('selects default implementation if interface is direct dependency', () async {
final Set<String> directDependencies = <String>{'url_launcher'};
final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(<Plugin>[
Plugin.fromYaml( Plugin.fromYaml(
'url_launcher', 'url_launcher_linux',
'', '',
YamlMap.wrap(<String, dynamic>{ YamlMap.wrap(<String, dynamic>{
'implements': 'url_launcher',
'platforms': <String, dynamic>{ 'platforms': <String, dynamic>{
'linux': <String, dynamic>{ 'linux': <String, dynamic>{
'default_package': 'url_launcher_linux', 'dartPluginClass': 'UrlLauncherPluginLinux',
}, },
}, },
}), }),
...@@ -348,13 +358,13 @@ void main() { ...@@ -348,13 +358,13 @@ void main() {
appDependencies: directDependencies, appDependencies: directDependencies,
), ),
Plugin.fromYaml( Plugin.fromYaml(
'url_launcher_linux', 'yet_another_url_launcher_linux',
'', '',
YamlMap.wrap(<String, dynamic>{ YamlMap.wrap(<String, dynamic>{
'implements': 'url_launcher', 'implements': 'url_launcher',
'platforms': <String, dynamic>{ 'platforms': <String, dynamic>{
'linux': <String, dynamic>{ 'linux': <String, dynamic>{
'dartPluginClass': 'UrlLauncherPluginLinux', 'dartPluginClass': 'UnofficialUrlLauncherPluginLinux2',
}, },
}, },
}), }),
...@@ -374,11 +384,8 @@ void main() { ...@@ -374,11 +384,8 @@ void main() {
); );
}); });
testWithoutContext('selects user selected implementation despites default implementation', () async { testWithoutContext('selects default implementation if interface is direct dependency', () async {
final Set<String> directDependencies = <String>{ final Set<String> directDependencies = <String>{'url_launcher'};
'user_selected_url_launcher_implementation',
'url_launcher',
};
final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(<Plugin>[ final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(<Plugin>[
Plugin.fromYaml( Plugin.fromYaml(
...@@ -412,27 +419,11 @@ void main() { ...@@ -412,27 +419,11 @@ void main() {
fileSystem: fs, fileSystem: fs,
appDependencies: directDependencies, appDependencies: directDependencies,
), ),
Plugin.fromYaml(
'user_selected_url_launcher_implementation',
'',
YamlMap.wrap(<String, dynamic>{
'implements': 'url_launcher',
'platforms': <String, dynamic>{
'linux': <String, dynamic>{
'dartPluginClass': 'UrlLauncherPluginLinux',
},
},
}),
null,
<String>[],
fileSystem: fs,
appDependencies: directDependencies,
),
]); ]);
expect(resolutions.length, equals(1)); expect(resolutions.length, equals(1));
expect(resolutions[0].toMap(), equals( expect(resolutions[0].toMap(), equals(
<String, String>{ <String, String>{
'pluginName': 'user_selected_url_launcher_implementation', 'pluginName': 'url_launcher_linux',
'dartClass': 'UrlLauncherPluginLinux', 'dartClass': 'UrlLauncherPluginLinux',
'platform': 'linux', 'platform': 'linux',
}) })
...@@ -545,22 +536,27 @@ void main() { ...@@ -545,22 +536,27 @@ void main() {
), ),
]); ]);
expect(
testLogger.errorText,
'Plugin `url_launcher_linux_2` implements an interface for `linux`, which was already implemented by plugin `url_launcher_linux_1`.\n'
'To fix this issue, remove either dependency from pubspec.yaml.'
'\n\n'
);
}, },
throwsToolExit( throwsToolExit(
message: 'Please resolve the errors', message: 'Please resolve the errors',
)); ));
expect(
testLogger.errorText,
'Plugin url_launcher:linux has conflicting direct dependency implementations:\n'
' url_launcher_linux_1\n'
' url_launcher_linux_2\n'
'To fix this issue, remove all but one of these dependencies from pubspec.yaml.'
'\n\n'
);
}); });
testUsingContext('provides all errors when user selected multiple implementations', () async { testUsingContext('provides all errors when user selected multiple implementations', () async {
final Set<String> directDependencies = <String>{ final Set<String> directDependencies = <String>{
'url_launcher_linux_1', 'url_launcher_linux_1',
'url_launcher_linux_2', 'url_launcher_linux_2',
'url_launcher_windows_1',
'url_launcher_windows_2',
}; };
expect(() { expect(() {
resolvePlatformImplementation(<Plugin>[ resolvePlatformImplementation(<Plugin>[
...@@ -596,18 +592,109 @@ void main() { ...@@ -596,18 +592,109 @@ void main() {
fileSystem: fs, fileSystem: fs,
appDependencies: directDependencies, appDependencies: directDependencies,
), ),
Plugin.fromYaml(
'url_launcher_windows_1',
'',
YamlMap.wrap(<String, dynamic>{
'implements': 'url_launcher',
'platforms': <String, dynamic>{
'windows': <String, dynamic>{
'dartPluginClass': 'UrlLauncherPluginWindows1',
},
},
}),
null,
<String>[],
fileSystem: fs,
appDependencies: directDependencies,
),
Plugin.fromYaml(
'url_launcher_windows_2',
'',
YamlMap.wrap(<String, dynamic>{
'implements': 'url_launcher',
'platforms': <String, dynamic>{
'windows': <String, dynamic>{
'dartPluginClass': 'UrlLauncherPluginWindows2',
},
},
}),
null,
<String>[],
fileSystem: fs,
appDependencies: directDependencies,
),
]); ]);
},
throwsToolExit(
message: 'Please resolve the errors',
));
expect( expect(
testLogger.errorText, testLogger.errorText,
'Plugin `url_launcher_linux_2` implements an interface for `linux`, which was already implemented by plugin `url_launcher_linux_1`.\n' 'Plugin url_launcher:linux has conflicting direct dependency implementations:\n'
'To fix this issue, remove either dependency from pubspec.yaml.' ' url_launcher_linux_1\n'
'\n\n' ' url_launcher_linux_2\n'
); 'To fix this issue, remove all but one of these dependencies from pubspec.yaml.'
'\n\n'
'Plugin url_launcher:windows has conflicting direct dependency implementations:\n'
' url_launcher_windows_1\n'
' url_launcher_windows_2\n'
'To fix this issue, remove all but one of these dependencies from pubspec.yaml.'
'\n\n'
);
});
testUsingContext('provides error when user needs to select among multiple implementations', () async {
final Set<String> directDependencies = <String>{};
expect(() {
resolvePlatformImplementation(<Plugin>[
Plugin.fromYaml(
'url_launcher_linux_1',
'',
YamlMap.wrap(<String, dynamic>{
'implements': 'url_launcher',
'platforms': <String, dynamic>{
'linux': <String, dynamic>{
'dartPluginClass': 'UrlLauncherPluginLinux1',
},
},
}),
null,
<String>[],
fileSystem: fs,
appDependencies: directDependencies,
),
Plugin.fromYaml(
'url_launcher_linux_2',
'',
YamlMap.wrap(<String, dynamic>{
'implements': 'url_launcher',
'platforms': <String, dynamic>{
'linux': <String, dynamic>{
'dartPluginClass': 'UrlLauncherPluginLinux2',
},
},
}),
null,
<String>[],
fileSystem: fs,
appDependencies: directDependencies,
),
]);
}, },
throwsToolExit( throwsToolExit(
message: 'Please resolve the errors', message: 'Please resolve the errors',
)); ));
expect(
testLogger.errorText,
'Plugin url_launcher:linux has multiple possible implementations:\n'
' url_launcher_linux_1\n'
' url_launcher_linux_2\n'
'To fix this issue, add one of these dependencies to pubspec.yaml.'
'\n\n'
);
}); });
}); });
...@@ -994,8 +1081,11 @@ void createFakeDartPlugins( ...@@ -994,8 +1081,11 @@ void createFakeDartPlugins(
) { ) {
final Directory fakePubCache = fs.systemTempDirectory.childDirectory('cache'); final Directory fakePubCache = fs.systemTempDirectory.childDirectory('cache');
final File packagesFile = flutterProject.directory final File packagesFile = flutterProject.directory
.childFile('.packages') .childFile('.packages');
..createSync(recursive: true); if (packagesFile.existsSync()) {
packagesFile.deleteSync();
}
packagesFile.createSync(recursive: true);
for (final MapEntry<String, String> entry in plugins.entries) { for (final MapEntry<String, String> entry in plugins.entries) {
final String name = fs.path.basename(entry.key); final String name = fs.path.basename(entry.key);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment