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
diff --git a/packages/flutter_tools/lib/src/flutter_plugins.dart b/packages/flutter_tools/lib/src/flutter_plugins.dart
index 9296293..9e4f42f 100644
--- a/packages/flutter_tools/lib/src/flutter_plugins.dart
+++ b/packages/flutter_tools/lib/src/flutter_plugins.dart
@@ -101,23 +101,23 @@
 const String _kFlutterPluginsPathKey = 'path';
 const String _kFlutterPluginsDependenciesKey = 'dependencies';
 
-  /// Filters [plugins] to those supported by [platformKey].
-  List<Map<String, dynamic>> _filterPluginsByPlatform(List<Plugin>plugins, String platformKey) {
-    final Iterable<Plugin> platformPlugins = plugins.where((Plugin p) {
-      return p.platforms.containsKey(platformKey);
-    });
+/// Filters [plugins] to those supported by [platformKey].
+List<Map<String, dynamic>> _filterPluginsByPlatform(List<Plugin> plugins, String platformKey) {
+  final Iterable<Plugin> platformPlugins = plugins.where((Plugin p) {
+    return p.platforms.containsKey(platformKey);
+  });
 
-    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;
+  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)],
+    });
   }
+  return pluginInfo;
+}
 
 /// 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
@@ -815,28 +815,43 @@
   }).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
 Future<void> writeWindowsPluginFiles(FlutterProject project, List<Plugin> plugins, TemplateRenderer templateRenderer) async {
   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>{
     'os': 'windows',
-    'plugins': windowsPlugins,
+    'plugins': pluginInfo,
     'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.windows),
   };
   await _writeCppPluginRegistrant(project.windows.managedDirectory, context, templateRenderer);
   await _writePluginCmakefile(project.windows.generatedPluginCmakeFile, context, templateRenderer);
 }
 
-/// The tooling currently treats UWP and win32 as identical for the
-/// purposes of tooling support and initial UWP bootstrap.
+/// The tooling currently treats UWP and win32 as identical, other than variant
+/// filtering, for the purposes of tooling support and initial UWP bootstrap.
 @visibleForTesting
 Future<void> writeWindowsUwpPluginFiles(FlutterProject project, List<Plugin> plugins, TemplateRenderer templateRenderer) async {
   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>{
     'os': 'windows',
-    'plugins': windowsPlugins,
+    'plugins': pluginInfo,
     'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.windowsUwp),
   };
   await _writeCppPluginRegistrant(project.windowsUwp.managedDirectory, context, templateRenderer);
diff --git a/packages/flutter_tools/lib/src/platform_plugins.dart b/packages/flutter_tools/lib/src/platform_plugins.dart
index dff6242..cd4f11c 100644
--- a/packages/flutter_tools/lib/src/platform_plugins.dart
+++ b/packages/flutter_tools/lib/src/platform_plugins.dart
@@ -16,6 +16,18 @@
 // Constant for 'defaultPackage' key in plugin maps.
 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.
 abstract class PluginPlatform {
   const PluginPlatform();
@@ -23,6 +35,12 @@
   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 {
   /// Determines whether the plugin has a native implementation or if it's a
   /// Dart-only plugin.
@@ -259,12 +277,13 @@
 ///
 /// 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.
-class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin{
+class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, VariantPlatformPlugin {
   const WindowsPlugin({
     required this.name,
     this.pluginClass,
     this.dartPluginClass,
     this.defaultPackage,
+    this.variants = const <PluginPlatformVariant>{},
   }) : assert(pluginClass != null || dartPluginClass != null || defaultPackage != null);
 
   factory WindowsPlugin.fromYaml(String name, YamlMap yaml) {
@@ -274,11 +293,31 @@
     if (pluginClass == 'none') {
       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(
       name: name,
       pluginClass: pluginClass,
       dartPluginClass: yaml[kDartPluginClass] as String?,
       defaultPackage: yaml[kDefaultPackage] as String?,
+      variants: variants,
     );
   }
 
@@ -286,6 +325,7 @@
     if (yaml == null) {
       return false;
     }
+
     return yaml[kPluginClass] is String ||
            yaml[kDartPluginClass] is String ||
            yaml[kDefaultPackage] is String;
@@ -297,6 +337,10 @@
   final String? pluginClass;
   final String? dartPluginClass;
   final String? defaultPackage;
+  final Set<PluginPlatformVariant> variants;
+
+  @override
+  Set<PluginPlatformVariant> get supportedVariants => variants;
 
   @override
   bool isNative() => pluginClass != null;
diff --git a/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart b/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart
index 490dc9a..b23b3fd 100644
--- a/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart
+++ b/packages/flutter_tools/test/general.shard/plugin_parsing_test.dart
@@ -253,6 +253,79 @@
     });
   });
 
+  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', () {
     final MemoryFileSystem fileSystem = MemoryFileSystem.test();
     final YamlMap? pluginYaml = loadYaml('') as YamlMap?;
diff --git a/packages/flutter_tools/test/general.shard/windows/plugins_test.dart b/packages/flutter_tools/test/general.shard/windows/plugins_test.dart
index 1eb14b8..0b3ca10 100644
--- a/packages/flutter_tools/test/general.shard/windows/plugins_test.dart
+++ b/packages/flutter_tools/test/general.shard/windows/plugins_test.dart
@@ -35,7 +35,7 @@
 
 void main() {
 
-  testWithoutContext('injects Win32 plugins', () async {
+  testWithoutContext('Win32 injects Win32 plugins', () async {
     final FileSystem fileSystem = MemoryFileSystem.test();
     setUpProject(fileSystem);
     final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory);
@@ -46,7 +46,12 @@
         path: 'foo',
         defaultPackagePlatforms: 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>[],
         isDirectDependency: true,
       ),
@@ -61,7 +66,7 @@
     );
   });
 
-  testWithoutContext('UWP injects Win32 plugins', () async {
+  testWithoutContext('UWP injects plugins marked as UWP-compatible', () async {
     final FileSystem fileSystem = MemoryFileSystem.test();
     setUpProject(fileSystem);
     final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory);
@@ -72,7 +77,12 @@
         path: 'foo',
         defaultPackagePlatforms: 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>[],
         isDirectDependency: true,
       ),
@@ -87,6 +97,37 @@
     );
   });
 
+  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', () {
     final FileSystem fileSystem = MemoryFileSystem.test();
     setUpProject(fileSystem);