Unverified Commit 57fcee28 authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Allow platform variants for Windows plugins (#82816)

Windows plugins are designed to share implementations between Win32 and
UWP, but not all plugins will support both. This adds a new
'supportedVariants' key to Windows plugins that allows specifying
'win32' and/or 'uwp' (and potentially others in the future in case that
becomes necessary).

Plugins without any supported variants will be assumed to be Win32 for
backward compatibility.

This will allow compiling Windows projects that use Win32-only Windows
plugins (which is currently all of them) in UWP mode. The plugins will
of course throw missing implementation exceptions at runtime, but tehy
won't prevent being able to build as they currently do.

Fixes https://github.com/flutter/flutter/issues/82815
parent 9343a9ae
...@@ -101,23 +101,23 @@ const String _kFlutterPluginsNameKey = 'name'; ...@@ -101,23 +101,23 @@ const String _kFlutterPluginsNameKey = 'name';
const String _kFlutterPluginsPathKey = 'path'; const String _kFlutterPluginsPathKey = 'path';
const String _kFlutterPluginsDependenciesKey = 'dependencies'; const String _kFlutterPluginsDependenciesKey = 'dependencies';
/// Filters [plugins] to those supported by [platformKey]. /// Filters [plugins] to those supported by [platformKey].
List<Map<String, dynamic>> _filterPluginsByPlatform(List<Plugin>plugins, String platformKey) { List<Map<String, dynamic>> _filterPluginsByPlatform(List<Plugin> plugins, String platformKey) {
final Iterable<Plugin> platformPlugins = plugins.where((Plugin p) { final Iterable<Plugin> platformPlugins = plugins.where((Plugin p) {
return p.platforms.containsKey(platformKey); return p.platforms.containsKey(platformKey);
});
final Set<String> pluginNames = platformPlugins.map((Plugin plugin) => plugin.name).toSet();
final List<Map<String, dynamic>> pluginInfo = <Map<String, dynamic>>[];
for (final Plugin plugin in platformPlugins) {
pluginInfo.add(<String, dynamic>{
_kFlutterPluginsNameKey: plugin.name,
_kFlutterPluginsPathKey: globals.fsUtils.escapePath(plugin.path),
_kFlutterPluginsDependenciesKey: <String>[...plugin.dependencies.where(pluginNames.contains)],
}); });
final Set<String> pluginNames = platformPlugins.map((Plugin plugin) => plugin.name).toSet();
final List<Map<String, dynamic>> list = <Map<String, dynamic>>[];
for (final Plugin plugin in platformPlugins) {
list.add(<String, dynamic>{
_kFlutterPluginsNameKey: plugin.name,
_kFlutterPluginsPathKey: globals.fsUtils.escapePath(plugin.path),
_kFlutterPluginsDependenciesKey: <String>[...plugin.dependencies.where(pluginNames.contains)],
});
}
return list;
} }
return pluginInfo;
}
/// Writes the .flutter-plugins-dependencies file based on the list of plugins. /// Writes the .flutter-plugins-dependencies file based on the list of plugins.
/// If there aren't any plugins, then the files aren't written to disk. The resulting /// If there aren't any plugins, then the files aren't written to disk. The resulting
...@@ -815,28 +815,43 @@ List<Plugin> _filterNativePlugins(List<Plugin> plugins, String platformKey) { ...@@ -815,28 +815,43 @@ List<Plugin> _filterNativePlugins(List<Plugin> plugins, String platformKey) {
}).toList(); }).toList();
} }
/// Returns only the plugins with the given platform variant.
List<Plugin> _filterPluginsByVariant(List<Plugin> plugins, String platformKey, PluginPlatformVariant variant) {
return plugins.where((Plugin element) {
final PluginPlatform platformPlugin = element.platforms[platformKey];
if (platformPlugin == null) {
return false;
}
assert(variant == null || platformPlugin is VariantPlatformPlugin);
return variant == null ||
(platformPlugin as VariantPlatformPlugin).supportedVariants.contains(variant);
}).toList();
}
@visibleForTesting @visibleForTesting
Future<void> writeWindowsPluginFiles(FlutterProject project, List<Plugin> plugins, TemplateRenderer templateRenderer) async { Future<void> writeWindowsPluginFiles(FlutterProject project, List<Plugin> plugins, TemplateRenderer templateRenderer) async {
final List<Plugin> nativePlugins = _filterNativePlugins(plugins, WindowsPlugin.kConfigKey); final List<Plugin> nativePlugins = _filterNativePlugins(plugins, WindowsPlugin.kConfigKey);
final List<Map<String, dynamic>> windowsPlugins = _extractPlatformMaps(nativePlugins, WindowsPlugin.kConfigKey); final List<Plugin> win32Plugins = _filterPluginsByVariant(nativePlugins, WindowsPlugin.kConfigKey, PluginPlatformVariant.win32);
final List<Map<String, dynamic>> pluginInfo = _extractPlatformMaps(win32Plugins, WindowsPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{ final Map<String, dynamic> context = <String, dynamic>{
'os': 'windows', 'os': 'windows',
'plugins': windowsPlugins, 'plugins': pluginInfo,
'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.windows), 'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.windows),
}; };
await _writeCppPluginRegistrant(project.windows.managedDirectory, context, templateRenderer); await _writeCppPluginRegistrant(project.windows.managedDirectory, context, templateRenderer);
await _writePluginCmakefile(project.windows.generatedPluginCmakeFile, context, templateRenderer); await _writePluginCmakefile(project.windows.generatedPluginCmakeFile, context, templateRenderer);
} }
/// The tooling currently treats UWP and win32 as identical for the /// The tooling currently treats UWP and win32 as identical, other than variant
/// purposes of tooling support and initial UWP bootstrap. /// filtering, for the purposes of tooling support and initial UWP bootstrap.
@visibleForTesting @visibleForTesting
Future<void> writeWindowsUwpPluginFiles(FlutterProject project, List<Plugin> plugins, TemplateRenderer templateRenderer) async { Future<void> writeWindowsUwpPluginFiles(FlutterProject project, List<Plugin> plugins, TemplateRenderer templateRenderer) async {
final List<Plugin> nativePlugins = _filterNativePlugins(plugins, WindowsPlugin.kConfigKey); final List<Plugin> nativePlugins = _filterNativePlugins(plugins, WindowsPlugin.kConfigKey);
final List<Map<String, dynamic>> windowsPlugins = _extractPlatformMaps(nativePlugins, WindowsPlugin.kConfigKey); final List<Plugin> uwpPlugins = _filterPluginsByVariant(nativePlugins, WindowsPlugin.kConfigKey, PluginPlatformVariant.winuwp);
final List<Map<String, dynamic>> pluginInfo = _extractPlatformMaps(uwpPlugins, WindowsPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{ final Map<String, dynamic> context = <String, dynamic>{
'os': 'windows', 'os': 'windows',
'plugins': windowsPlugins, 'plugins': pluginInfo,
'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.windowsUwp), 'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.windowsUwp),
}; };
await _writeCppPluginRegistrant(project.windowsUwp.managedDirectory, context, templateRenderer); await _writeCppPluginRegistrant(project.windowsUwp.managedDirectory, context, templateRenderer);
......
...@@ -16,6 +16,18 @@ const String kDartPluginClass = 'dartPluginClass'; ...@@ -16,6 +16,18 @@ const String kDartPluginClass = 'dartPluginClass';
// Constant for 'defaultPackage' key in plugin maps. // Constant for 'defaultPackage' key in plugin maps.
const String kDefaultPackage = 'default_package'; const String kDefaultPackage = 'default_package';
/// Constant for 'supportedVariants' key in plugin maps.
const String kSupportedVariants = 'supportedVariants';
/// Platform variants that a Windows plugin can support.
enum PluginPlatformVariant {
/// Win32 variant of Windows.
win32,
// UWP variant of Windows.
winuwp,
}
/// Marker interface for all platform specific plugin config implementations. /// Marker interface for all platform specific plugin config implementations.
abstract class PluginPlatform { abstract class PluginPlatform {
const PluginPlatform(); const PluginPlatform();
...@@ -23,6 +35,12 @@ abstract class PluginPlatform { ...@@ -23,6 +35,12 @@ abstract class PluginPlatform {
Map<String, dynamic> toMap(); Map<String, dynamic> toMap();
} }
/// A plugin that has platform variants.
abstract class VariantPlatformPlugin {
/// The platform variants supported by the plugin.
Set<PluginPlatformVariant> get supportedVariants;
}
abstract class NativeOrDartPlugin { abstract class NativeOrDartPlugin {
/// Determines whether the plugin has a native implementation or if it's a /// Determines whether the plugin has a native implementation or if it's a
/// Dart-only plugin. /// Dart-only plugin.
...@@ -259,12 +277,13 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin { ...@@ -259,12 +277,13 @@ class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin {
/// ///
/// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required. /// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required.
/// [pluginClass] will be the entry point to the plugin's native code. /// [pluginClass] will be the entry point to the plugin's native code.
class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, VariantPlatformPlugin {
const WindowsPlugin({ const WindowsPlugin({
required this.name, required this.name,
this.pluginClass, this.pluginClass,
this.dartPluginClass, this.dartPluginClass,
this.defaultPackage, this.defaultPackage,
this.variants = const <PluginPlatformVariant>{},
}) : assert(pluginClass != null || dartPluginClass != null || defaultPackage != null); }) : assert(pluginClass != null || dartPluginClass != null || defaultPackage != null);
factory WindowsPlugin.fromYaml(String name, YamlMap yaml) { factory WindowsPlugin.fromYaml(String name, YamlMap yaml) {
...@@ -274,11 +293,31 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{ ...@@ -274,11 +293,31 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{
if (pluginClass == 'none') { if (pluginClass == 'none') {
pluginClass = null; pluginClass = null;
} }
final Set<PluginPlatformVariant> variants = <PluginPlatformVariant>{};
final YamlList? variantList = yaml[kSupportedVariants] as YamlList?;
if (variantList == null) {
// If no variant list is provided assume Win32 for backward compatibility.
variants.add(PluginPlatformVariant.win32);
} else {
const Map<String, PluginPlatformVariant> variantByName = <String, PluginPlatformVariant>{
'win32': PluginPlatformVariant.win32,
'uwp': PluginPlatformVariant.winuwp,
};
for (final String variantName in variantList.cast<String>()) {
final PluginPlatformVariant? variant = variantByName[variantName];
if (variant != null) {
variants.add(variant);
}
// Ignore unrecognized variants to make adding new variants in the
// future non-breaking.
}
}
return WindowsPlugin( return WindowsPlugin(
name: name, name: name,
pluginClass: pluginClass, pluginClass: pluginClass,
dartPluginClass: yaml[kDartPluginClass] as String?, dartPluginClass: yaml[kDartPluginClass] as String?,
defaultPackage: yaml[kDefaultPackage] as String?, defaultPackage: yaml[kDefaultPackage] as String?,
variants: variants,
); );
} }
...@@ -286,6 +325,7 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{ ...@@ -286,6 +325,7 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{
if (yaml == null) { if (yaml == null) {
return false; return false;
} }
return yaml[kPluginClass] is String || return yaml[kPluginClass] is String ||
yaml[kDartPluginClass] is String || yaml[kDartPluginClass] is String ||
yaml[kDefaultPackage] is String; yaml[kDefaultPackage] is String;
...@@ -297,6 +337,10 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{ ...@@ -297,6 +337,10 @@ class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{
final String? pluginClass; final String? pluginClass;
final String? dartPluginClass; final String? dartPluginClass;
final String? defaultPackage; final String? defaultPackage;
final Set<PluginPlatformVariant> variants;
@override
Set<PluginPlatformVariant> get supportedVariants => variants;
@override @override
bool isNative() => pluginClass != null; bool isNative() => pluginClass != null;
......
...@@ -253,6 +253,79 @@ void main() { ...@@ -253,6 +253,79 @@ void main() {
}); });
}); });
testWithoutContext('Windows allows supported mode lists', () {
final FileSystem fileSystem = MemoryFileSystem.test();
const String pluginYamlRaw =
'platforms:\n'
' windows:\n'
' pluginClass: WinSamplePlugin\n'
' supportedVariants:\n'
' - win32\n'
' - uwp\n';
final YamlMap pluginYaml = loadYaml(pluginYamlRaw) as YamlMap;
final Plugin plugin = Plugin.fromYaml(
_kTestPluginName,
_kTestPluginPath,
pluginYaml,
const <String>[],
fileSystem: fileSystem,
);
final WindowsPlugin windowsPlugin = plugin.platforms[WindowsPlugin.kConfigKey]! as WindowsPlugin;
expect(windowsPlugin.supportedVariants, <PluginPlatformVariant>[
PluginPlatformVariant.win32,
PluginPlatformVariant.winuwp,
]);
});
testWithoutContext('Windows assumes win32 when no variants are given', () {
final FileSystem fileSystem = MemoryFileSystem.test();
const String pluginYamlRaw =
'platforms:\n'
' windows:\n'
' pluginClass: WinSamplePlugin\n';
final YamlMap pluginYaml = loadYaml(pluginYamlRaw) as YamlMap;
final Plugin plugin = Plugin.fromYaml(
_kTestPluginName,
_kTestPluginPath,
pluginYaml,
const <String>[],
fileSystem: fileSystem,
);
final WindowsPlugin windowsPlugin = plugin.platforms[WindowsPlugin.kConfigKey]! as WindowsPlugin;
expect(windowsPlugin.supportedVariants, <PluginPlatformVariant>[
PluginPlatformVariant.win32,
]);
});
testWithoutContext('Windows ignores unknown variants', () {
final FileSystem fileSystem = MemoryFileSystem.test();
const String pluginYamlRaw =
'platforms:\n'
' windows:\n'
' pluginClass: WinSamplePlugin\n'
' supportedVariants:\n'
' - not_yet_invented_variant\n'
' - uwp\n';
final YamlMap pluginYaml = loadYaml(pluginYamlRaw) as YamlMap;
final Plugin plugin = Plugin.fromYaml(
_kTestPluginName,
_kTestPluginPath,
pluginYaml,
const <String>[],
fileSystem: fileSystem,
);
final WindowsPlugin windowsPlugin = plugin.platforms[WindowsPlugin.kConfigKey]! as WindowsPlugin;
expect(windowsPlugin.supportedVariants, <PluginPlatformVariant>{
PluginPlatformVariant.winuwp,
});
});
testWithoutContext('Plugin parsing throws a fatal error on an empty plugin', () { testWithoutContext('Plugin parsing throws a fatal error on an empty plugin', () {
final MemoryFileSystem fileSystem = MemoryFileSystem.test(); final MemoryFileSystem fileSystem = MemoryFileSystem.test();
final YamlMap? pluginYaml = loadYaml('') as YamlMap?; final YamlMap? pluginYaml = loadYaml('') as YamlMap?;
......
...@@ -35,7 +35,7 @@ const String kPluginDependencies = r''' ...@@ -35,7 +35,7 @@ const String kPluginDependencies = r'''
void main() { void main() {
testWithoutContext('injects Win32 plugins', () async { testWithoutContext('Win32 injects Win32 plugins', () async {
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
setUpProject(fileSystem); setUpProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory);
...@@ -46,7 +46,12 @@ void main() { ...@@ -46,7 +46,12 @@ void main() {
path: 'foo', path: 'foo',
defaultPackagePlatforms: const <String, String>{}, defaultPackagePlatforms: const <String, String>{},
pluginDartClassPlatforms: const <String, String>{}, pluginDartClassPlatforms: const <String, String>{},
platforms: const <String, PluginPlatform>{WindowsPlugin.kConfigKey: WindowsPlugin(name: 'test', pluginClass: 'Foo')}, platforms: const <String, PluginPlatform>{
WindowsPlugin.kConfigKey: WindowsPlugin(
name: 'test',
pluginClass: 'Foo',
variants: <PluginPlatformVariant>{PluginPlatformVariant.win32},
)},
dependencies: <String>[], dependencies: <String>[],
isDirectDependency: true, isDirectDependency: true,
), ),
...@@ -61,7 +66,7 @@ void main() { ...@@ -61,7 +66,7 @@ void main() {
); );
}); });
testWithoutContext('UWP injects Win32 plugins', () async { testWithoutContext('UWP injects plugins marked as UWP-compatible', () async {
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
setUpProject(fileSystem); setUpProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory);
...@@ -72,7 +77,12 @@ void main() { ...@@ -72,7 +77,12 @@ void main() {
path: 'foo', path: 'foo',
defaultPackagePlatforms: const <String, String>{}, defaultPackagePlatforms: const <String, String>{},
pluginDartClassPlatforms: const <String, String>{}, pluginDartClassPlatforms: const <String, String>{},
platforms: const <String, PluginPlatform>{WindowsPlugin.kConfigKey: WindowsPlugin(name: 'test', pluginClass: 'Foo')}, platforms: const <String, PluginPlatform>{
WindowsPlugin.kConfigKey: WindowsPlugin(
name: 'test',
pluginClass: 'Foo',
variants: <PluginPlatformVariant>{PluginPlatformVariant.winuwp},
)},
dependencies: <String>[], dependencies: <String>[],
isDirectDependency: true, isDirectDependency: true,
), ),
...@@ -87,6 +97,37 @@ void main() { ...@@ -87,6 +97,37 @@ void main() {
); );
}); });
testWithoutContext('UWP does not inject Win32-only plugins', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
setUpProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory);
await writeWindowsUwpPluginFiles(flutterProject, <Plugin>[
Plugin(
name: 'test',
path: 'foo',
defaultPackagePlatforms: const <String, String>{},
pluginDartClassPlatforms: const <String, String>{},
platforms: const <String, PluginPlatform>{
WindowsPlugin.kConfigKey: WindowsPlugin(
name: 'test',
pluginClass: 'Foo',
variants: <PluginPlatformVariant>{PluginPlatformVariant.win32},
)},
dependencies: <String>[],
isDirectDependency: true,
),
], renderer);
final Directory managed = flutterProject.windowsUwp.managedDirectory;
expect(flutterProject.windowsUwp.generatedPluginCmakeFile, exists);
expect(managed.childFile('generated_plugin_registrant.h'), exists);
expect(
managed.childFile('generated_plugin_registrant.cc').readAsStringSync(),
isNot(contains('#include <test/foo.h>')),
);
});
testWithoutContext('Symlink injection treats UWP as Win32', () { testWithoutContext('Symlink injection treats UWP as Win32', () {
final FileSystem fileSystem = MemoryFileSystem.test(); final FileSystem fileSystem = MemoryFileSystem.test();
setUpProject(fileSystem); setUpProject(fileSystem);
......
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