Unverified Commit 56d68a90 authored by stuartmorgan's avatar stuartmorgan Committed by GitHub

Add the beginnings of plugin support for Windows and Linux (#41015)

Adds very preliminary support for Windows and Linux plugins:
- Adds those platforms to the new plugin schema, initially supporting just a plugin class.
- Adds C++ plugin registrant generation for any Windows or Linux plugins found.

This doesn't have yet have any build tooling for either platform, so anyone using the generated registrant still needs to do manual build configuration. This reduces the manual work, however, and creates a starting point for future tooling work.

As with all Windows and Linux work at this time, this is not final, and subject to change without warning in the future (e.g., Windows could potentially switch to a C# interface, or
'linux' may change to 'gtk' or 'linux_gtk' in pubspec.yaml).
parent 5a27e1c1
......@@ -140,6 +140,86 @@ class MacOSPlugin extends PluginPlatform {
}
}
/// Contains the parameters to template a Windows plugin.
///
/// The required fields include: [name] of the plugin, and [pluginClass] that will
/// be the entry point to the plugin's native code.
class WindowsPlugin extends PluginPlatform {
const WindowsPlugin({
@required this.name,
@required this.pluginClass,
});
factory WindowsPlugin.fromYaml(String name, YamlMap yaml) {
assert(validate(yaml));
return WindowsPlugin(
name: name,
pluginClass: yaml['pluginClass'],
);
}
static bool validate(YamlMap yaml) {
if (yaml == null) {
return false;
}
return yaml['pluginClass'] is String;
}
static const String kConfigKey = 'windows';
final String name;
final String pluginClass;
@override
Map<String, dynamic> toMap() {
return <String, dynamic>{
'name': name,
'class': pluginClass,
'filename': _filenameForCppClass(pluginClass),
};
}
}
/// Contains the parameters to template a Linux plugin.
///
/// The required fields include: [name] of the plugin, and [pluginClass] that will
/// be the entry point to the plugin's native code.
class LinuxPlugin extends PluginPlatform {
const LinuxPlugin({
@required this.name,
@required this.pluginClass,
});
factory LinuxPlugin.fromYaml(String name, YamlMap yaml) {
assert(validate(yaml));
return LinuxPlugin(
name: name,
pluginClass: yaml['pluginClass'],
);
}
static bool validate(YamlMap yaml) {
if (yaml == null) {
return false;
}
return yaml['pluginClass'] is String;
}
static const String kConfigKey = 'linux';
final String name;
final String pluginClass;
@override
Map<String, dynamic> toMap() {
return <String, dynamic>{
'name': name,
'class': pluginClass,
'filename': _filenameForCppClass(pluginClass),
};
}
}
/// Contains the parameters to template a web plugin.
///
/// The required fields include: [name] of the plugin, the [pluginClass] that will
......@@ -190,3 +270,11 @@ class WebPlugin extends PluginPlatform {
};
}
}
final RegExp _internalCapitalLetterRegex = RegExp(r'(?=(?!^)[A-Z])');
String _filenameForCppClass(String className) {
return className.splitMapJoin(
_internalCapitalLetterRegex,
onMatch: (_) => '_',
onNonMatch: (String n) => n.toLowerCase());
}
......@@ -50,8 +50,12 @@ class Plugin {
/// pluginClass: SamplePlugin
/// ios:
/// pluginClass: SamplePlugin
/// linux:
/// pluginClass: SamplePlugin
/// macos:
/// pluginClass: SamplePlugin
/// windows:
/// pluginClass: SamplePlugin
factory Plugin.fromYaml(String name, String path, dynamic pluginYaml) {
final List<String> errors = validatePluginYaml(pluginYaml);
if (errors.isNotEmpty) {
......@@ -84,6 +88,11 @@ class Plugin {
IOSPlugin.fromYaml(name, platformsYaml[IOSPlugin.kConfigKey]);
}
if (platformsYaml[LinuxPlugin.kConfigKey] != null) {
platforms[LinuxPlugin.kConfigKey] =
LinuxPlugin.fromYaml(name, platformsYaml[LinuxPlugin.kConfigKey]);
}
if (platformsYaml[MacOSPlugin.kConfigKey] != null) {
platforms[MacOSPlugin.kConfigKey] =
MacOSPlugin.fromYaml(name, platformsYaml[MacOSPlugin.kConfigKey]);
......@@ -94,6 +103,11 @@ class Plugin {
WebPlugin.fromYaml(name, platformsYaml[WebPlugin.kConfigKey]);
}
if (platformsYaml[WindowsPlugin.kConfigKey] != null) {
platforms[WindowsPlugin.kConfigKey] =
WindowsPlugin.fromYaml(name, platformsYaml[WindowsPlugin.kConfigKey]);
}
return Plugin(
name: name,
path: path,
......@@ -156,10 +170,18 @@ class Plugin {
!IOSPlugin.validate(yaml[IOSPlugin.kConfigKey])) {
errors.add('Invalid "ios" plugin specification.');
}
if (yaml.containsKey(LinuxPlugin.kConfigKey) &&
!LinuxPlugin.validate(yaml[LinuxPlugin.kConfigKey])) {
errors.add('Invalid "linux" plugin specification.');
}
if (yaml.containsKey(MacOSPlugin.kConfigKey) &&
!MacOSPlugin.validate(yaml[MacOSPlugin.kConfigKey])) {
errors.add('Invalid "macos" plugin specification.');
}
if (yaml.containsKey(WindowsPlugin.kConfigKey) &&
!WindowsPlugin.validate(yaml[WindowsPlugin.kConfigKey])) {
errors.add('Invalid "windows" plugin specification.');
}
return errors;
}
......@@ -171,9 +193,6 @@ class Plugin {
if (yaml['iosPrefix'] != null && yaml['iosPrefix'] is! String) {
errors.add('The "iosPrefix" must either be null or a string.');
}
if (yaml['macosPrefix'] != null && yaml['macosPrefix'] is! String) {
errors.add('The "macosPrefix" must either be null or a string.');
}
if (yaml['pluginClass'] != null && yaml['pluginClass'] is! String) {
errors.add('The "pluginClass" must either be null or a string..');
}
......@@ -415,6 +434,39 @@ void registerPlugins(PluginRegistry registry) {
}
''';
const String _cppPluginRegistryHeaderTemplate = '''//
// Generated file. Do not edit.
//
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_
''';
const String _cppPluginRegistryImplementationTemplate = '''//
// Generated file. Do not edit.
//
#include "generated_plugin_registrant.h"
{{#plugins}}
#include <{{filename}}.h>
{{/plugins}}
void RegisterPlugins(flutter::PluginRegistry* registry) {
{{#plugins}}
{{class}}RegisterWithRegistrar(
registry->GetRegistrarForPlugin("{{class}}"));
{{/plugins}}
}
''';
Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Map<String, dynamic>> iosPlugins = _extractPlatformMaps(plugins, IOSPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{
......@@ -455,6 +507,14 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug
}
}
Future<void> _writeLinuxPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Map<String, dynamic>> linuxPlugins = _extractPlatformMaps(plugins, LinuxPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{
'plugins': linuxPlugins,
};
await _writeCppPluginRegistrant(project.linux.managedDirectory, context);
}
Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Map<String, dynamic>> macosPlugins = _extractPlatformMaps(plugins, MacOSPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{
......@@ -470,6 +530,28 @@ Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> pl
);
}
Future<void> _writeWindowsPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Map<String, dynamic>> windowsPlugins = _extractPlatformMaps(plugins, WindowsPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{
'plugins': windowsPlugins,
};
await _writeCppPluginRegistrant(project.windows.managedDirectory, context);
}
Future<void> _writeCppPluginRegistrant(Directory destination, Map<String, dynamic> templateContext) async {
final String registryDirectory = destination.path;
_renderTemplateToFile(
_cppPluginRegistryHeaderTemplate,
templateContext,
fs.path.join(registryDirectory, 'generated_plugin_registrant.h'),
);
_renderTemplateToFile(
_cppPluginRegistryImplementationTemplate,
templateContext,
fs.path.join(registryDirectory, 'generated_plugin_registrant.cc'),
);
}
Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Map<String, dynamic>> webPlugins = _extractPlatformMaps(plugins, WebPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{
......@@ -527,12 +609,18 @@ Future<void> injectPlugins(FlutterProject project, {bool checkProjects = false})
if ((checkProjects && project.ios.existsSync()) || !checkProjects) {
await _writeIOSPluginRegistrant(project, plugins);
}
// TODO(stuartmorgan): Revisit the condition here once the plans for handling
// TODO(stuartmorgan): Revisit the conditions here once the plans for handling
// desktop in existing projects are in place. For now, ignore checkProjects
// on desktop and always treat it as true.
if (featureFlags.isLinuxEnabled && project.linux.existsSync()) {
await _writeLinuxPluginRegistrant(project, plugins);
}
if (featureFlags.isMacOSEnabled && project.macos.existsSync()) {
await _writeMacOSPluginRegistrant(project, plugins);
}
if (featureFlags.isWindowsEnabled && project.windows.existsSync()) {
await _writeWindowsPluginRegistrant(project, plugins);
}
for (final XcodeBasedProject subproject in <XcodeBasedProject>[project.ios, project.macos]) {
if (!project.isModule && (!checkProjects || subproject.existsSync())) {
final CocoaPods cocoaPods = CocoaPods();
......
......@@ -207,11 +207,18 @@ class FlutterProject {
if ((ios.existsSync() && checkProjects) || !checkProjects) {
await ios.ensureReadyForPlatformSpecificTooling();
}
// TODO(stuartmorgan): Add checkProjects logic once a create workflow exists
// for macOS. For now, always treat checkProjects as true for macOS.
// TODO(stuartmorgan): Revisit conditions once there is a plan for handling
// non-default platform projects. For now, always treat checkProjects as
// true for desktop.
if (featureFlags.isLinuxEnabled && linux.existsSync()) {
await linux.ensureReadyForPlatformSpecificTooling();
}
if (featureFlags.isMacOSEnabled && macos.existsSync()) {
await macos.ensureReadyForPlatformSpecificTooling();
}
if (featureFlags.isWindowsEnabled && windows.existsSync()) {
await windows.ensureReadyForPlatformSpecificTooling();
}
if (featureFlags.isWebEnabled && web.existsSync()) {
await web.ensureReadyForPlatformSpecificTooling();
}
......@@ -790,6 +797,8 @@ class WindowsProject {
///
/// Ideally this will be replaced in the future with inspection of the project.
File get nameFile => ephemeralDirectory.childFile('exe_filename');
Future<void> ensureReadyForPlatformSpecificTooling() async {}
}
/// The Linux sub project.
......@@ -818,6 +827,8 @@ class LinuxProject {
/// Contains definitions for FLUTTER_ROOT, LOCAL_ENGINE, and more flags for
/// the build.
File get generatedMakeConfigFile => ephemeralDirectory.childFile('generated_config.mk');
Future<void> ensureReadyForPlatformSpecificTooling() async {}
}
/// The Fuchisa sub project
......
......@@ -76,12 +76,26 @@
"pluginClass": {"type": "string"}
}
},
"linux": {
"type": "object",
"additionalProperties": false,
"properties": {
"pluginClass": {"type": "string"}
}
},
"macos": {
"type": "object",
"additionalProperties": false,
"properties": {
"pluginClass": {"type": "string"}
}
},
"windows": {
"type": "object",
"additionalProperties": false,
"properties": {
"pluginClass": {"type": "string"}
}
}
}
},
......
......@@ -36,16 +36,20 @@ void main() {
test('Multi-platform Format', () {
const String pluginYamlRaw = 'platforms:\n'
' macos:\n'
' pluginClass: MSamplePlugin\n'
' android:\n'
' package: com.flutter.dev\n'
' pluginClass: ASamplePlugin\n'
' ios:\n'
' pluginClass: ISamplePlugin\n'
' linux:\n'
' pluginClass: LSamplePlugin\n'
' macos:\n'
' pluginClass: MSamplePlugin\n'
' web:\n'
' pluginClass: WSamplePlugin\n'
' fileName: web_plugin.dart\n';
' pluginClass: WebSamplePlugin\n'
' fileName: web_plugin.dart\n'
' windows:\n'
' pluginClass: WinSamplePlugin\n';
final dynamic pluginYaml = loadYaml(pluginYamlRaw);
final Plugin plugin =
......@@ -53,10 +57,14 @@ void main() {
final AndroidPlugin androidPlugin =
plugin.platforms[AndroidPlugin.kConfigKey];
final IOSPlugin iosPlugin = plugin.platforms[IOSPlugin.kConfigKey];
final LinuxPlugin linuxPlugin =
plugin.platforms[LinuxPlugin.kConfigKey];
final MacOSPlugin macOSPlugin =
plugin.platforms[MacOSPlugin.kConfigKey];
final IOSPlugin iosPlugin = plugin.platforms[IOSPlugin.kConfigKey];
final WebPlugin webPlugin = plugin.platforms[WebPlugin.kConfigKey];
final WindowsPlugin windowsPlugin =
plugin.platforms[WindowsPlugin.kConfigKey];
final String androidPluginClass = androidPlugin.pluginClass;
final String iosPluginClass = iosPlugin.pluginClass;
......@@ -64,9 +72,11 @@ void main() {
expect(androidPluginClass, 'ASamplePlugin');
expect(iosPlugin.classPrefix, '');
expect(androidPlugin.package, 'com.flutter.dev');
expect(linuxPlugin.pluginClass, 'LSamplePlugin');
expect(macOSPlugin.pluginClass, 'MSamplePlugin');
expect(webPlugin.pluginClass, 'WSamplePlugin');
expect(webPlugin.pluginClass, 'WebSamplePlugin');
expect(webPlugin.fileName, 'web_plugin.dart');
expect(windowsPlugin.pluginClass, 'WinSamplePlugin');
});
});
}
......@@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/context.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/flutter_manifest.dart';
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
......@@ -188,6 +189,48 @@ void main() {
await project.ensureReadyForPlatformSpecificTooling();
expectExists(project.android.hostAppGradleRoot.childFile('local.properties'));
});
testUsingContext('injects plugins for macOS', () async {
final FlutterProject project = await someProject();
project.macos.managedDirectory.createSync(recursive: true);
await project.ensureReadyForPlatformSpecificTooling();
expectExists(project.macos.managedDirectory.childFile('GeneratedPluginRegistrant.swift'));
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
FlutterProjectFactory: () => FlutterProjectFactory(),
});
testUsingContext('generates Xcode configuration for macOS', () async {
final FlutterProject project = await someProject();
project.macos.managedDirectory.createSync(recursive: true);
await project.ensureReadyForPlatformSpecificTooling();
expectExists(project.macos.generatedXcodePropertiesFile);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
FeatureFlags: () => TestFeatureFlags(isMacOSEnabled: true),
FlutterProjectFactory: () => FlutterProjectFactory(),
});
testUsingContext('injects plugins for Linux', () async {
final FlutterProject project = await someProject();
project.linux.managedDirectory.createSync(recursive: true);
await project.ensureReadyForPlatformSpecificTooling();
expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.h'));
expectExists(project.linux.managedDirectory.childFile('generated_plugin_registrant.cc'));
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
FeatureFlags: () => TestFeatureFlags(isLinuxEnabled: true),
FlutterProjectFactory: () => FlutterProjectFactory(),
});
testUsingContext('injects plugins for Windows', () async {
final FlutterProject project = await someProject();
project.windows.managedDirectory.createSync(recursive: true);
await project.ensureReadyForPlatformSpecificTooling();
expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.h'));
expectExists(project.windows.managedDirectory.childFile('generated_plugin_registrant.cc'));
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
FeatureFlags: () => TestFeatureFlags(isWindowsEnabled: true),
FlutterProjectFactory: () => FlutterProjectFactory(),
});
testInMemory('creates Android library in module', () async {
final FlutterProject project = await aModuleProject();
await project.ensureReadyForPlatformSpecificTooling();
......@@ -512,8 +555,12 @@ flutter:
pluginClass: MyPlugin
ios:
pluginClass: MyPlugin
linux:
pluginClass: MyPlugin
macos:
pluginClass: MyPlugin
windows:
pluginClass: MyPlugin
''';
}
directory.childFile('pubspec.yaml').writeAsStringSync(pluginPubSpec);
......
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