Unverified Commit fc05c373 authored by Kaushik Iska's avatar Kaushik Iska Committed by GitHub

Flutter Plugin Tool supports multi-platform plugin config (#38632)

parent b2a4ebe3
...@@ -11,6 +11,7 @@ import 'base/user_messages.dart'; ...@@ -11,6 +11,7 @@ import 'base/user_messages.dart';
import 'base/utils.dart'; import 'base/utils.dart';
import 'cache.dart'; import 'cache.dart';
import 'globals.dart'; import 'globals.dart';
import 'plugins.dart';
/// A wrapper around the `flutter` section in the `pubspec.yaml` file. /// A wrapper around the `flutter` section in the `pubspec.yaml` file.
class FlutterManifest { class FlutterManifest {
...@@ -151,8 +152,14 @@ class FlutterManifest { ...@@ -151,8 +152,14 @@ class FlutterManifest {
String get androidPackage { String get androidPackage {
if (isModule) if (isModule)
return _flutterDescriptor['module']['androidPackage']; return _flutterDescriptor['module']['androidPackage'];
if (isPlugin) if (isPlugin) {
return _flutterDescriptor['plugin']['androidPackage']; final YamlMap plugin = _flutterDescriptor['plugin'];
if (plugin.containsKey('platforms')) {
return plugin['platforms']['android']['package'];
} else {
return plugin['androidPackage'];
}
}
return null; return null;
} }
...@@ -378,18 +385,8 @@ void _validateFlutter(YamlMap yaml, List<String> errors) { ...@@ -378,18 +385,8 @@ void _validateFlutter(YamlMap yaml, List<String> errors) {
if (kvp.value is! YamlMap) { if (kvp.value is! YamlMap) {
errors.add('Expected "${kvp.key}" to be an object, but got ${kvp.value} (${kvp.value.runtimeType}).'); errors.add('Expected "${kvp.key}" to be an object, but got ${kvp.value} (${kvp.value.runtimeType}).');
} }
if (kvp.value['androidPackage'] != null && kvp.value['androidPackage'] is! String) { final List<String> pluginErrors = Plugin.validatePluginYaml(kvp.value);
errors.add('The "androidPackage" must either be null or a string.'); errors.addAll(pluginErrors);
}
if (kvp.value['iosPrefix'] != null && kvp.value['iosPrefix'] is! String) {
errors.add('The "iosPrefix" must either be null or a string.');
}
if (kvp.value['macosPrefix'] != null && kvp.value['macosPrefix'] is! String) {
errors.add('The "macosPrefix" must either be null or a string.');
}
if (kvp.value['pluginClass'] != null && kvp.value['pluginClass'] is! String) {
errors.add('The "pluginClass" must either be null or a string..');
}
break; break;
default: default:
errors.add('Unexpected child "${kvp.key}" found under "flutter".'); errors.add('Unexpected child "${kvp.key}" found under "flutter".');
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:meta/meta.dart';
import 'package:yaml/yaml.dart';
/// Marker interface for all platform specific plugin config impls.
abstract class PluginPlatform {
const PluginPlatform();
Map<String, dynamic> toMap();
}
/// Contains parameters to template an Android plugin.
///
/// The required fields include: [name] of the plugin, [package] of the plugin and
/// the [pluginClass] that will be the entry point to the plugin's native code.
class AndroidPlugin extends PluginPlatform {
const AndroidPlugin({
@required this.name,
@required this.package,
@required this.pluginClass,
});
factory AndroidPlugin.fromYaml(String name, YamlMap yaml) {
assert(validate(yaml));
return AndroidPlugin(
name: name,
package: yaml['package'],
pluginClass: yaml['pluginClass'],
);
}
static bool validate(YamlMap yaml) {
if (yaml == null) {
return false;
}
return yaml['package'] is String && yaml['pluginClass'] is String;
}
static const String kConfigKey = 'android';
final String name;
final String package;
final String pluginClass;
@override
Map<String, dynamic> toMap() {
return <String, dynamic>{
'name': name,
'package': package,
'class': pluginClass,
};
}
}
/// Contains the parameters to template an iOS plugin.
///
/// The required fields include: [name] of the plugin, the [pluginClass] that
/// will be the entry point to the plugin's native code.
class IOSPlugin extends PluginPlatform {
const IOSPlugin({
@required this.name,
this.classPrefix,
@required this.pluginClass,
});
factory IOSPlugin.fromYaml(String name, YamlMap yaml) {
assert(validate(yaml));
return IOSPlugin(
name: name,
classPrefix: '',
pluginClass: yaml['pluginClass'],
);
}
static bool validate(YamlMap yaml) {
if (yaml == null) {
return false;
}
return yaml['pluginClass'] is String;
}
static const String kConfigKey = 'ios';
final String name;
/// Note, this is here only for legacy reasons. Multi-platform format
/// always sets it to empty String.
final String classPrefix;
final String pluginClass;
@override
Map<String, dynamic> toMap() {
return <String, dynamic>{
'name': name,
'prefix': classPrefix,
'class': pluginClass,
};
}
}
/// Contains the parameters to template a macOS plugin.
///
/// The required fields include: [name] of the plugin, and [pluginClass] that will
/// be the entry point to the plugin's native code.
class MacOSPlugin extends PluginPlatform {
const MacOSPlugin({
@required this.name,
@required this.pluginClass,
});
factory MacOSPlugin.fromYaml(String name, YamlMap yaml) {
assert(validate(yaml));
return MacOSPlugin(
name: name,
pluginClass: yaml['pluginClass'],
);
}
static bool validate(YamlMap yaml) {
if (yaml == null) {
return false;
}
return yaml['pluginClass'] is String;
}
static const String kConfigKey = 'macos';
final String name;
final String pluginClass;
@override
Map<String, dynamic> toMap() {
return <String, dynamic>{
'name': name,
'class': pluginClass,
};
}
}
...@@ -7,11 +7,13 @@ import 'dart:async'; ...@@ -7,11 +7,13 @@ import 'dart:async';
import 'package:mustache/mustache.dart' as mustache; import 'package:mustache/mustache.dart' as mustache;
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
import 'base/common.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'dart/package_map.dart'; import 'dart/package_map.dart';
import 'features.dart'; import 'features.dart';
import 'globals.dart'; import 'globals.dart';
import 'macos/cocoapods.dart'; import 'macos/cocoapods.dart';
import 'platform_plugins.dart';
import 'project.dart'; import 'project.dart';
void _renderTemplateToFile(String template, dynamic context, String filePath) { void _renderTemplateToFile(String template, dynamic context, String filePath) {
...@@ -26,41 +28,158 @@ class Plugin { ...@@ -26,41 +28,158 @@ class Plugin {
Plugin({ Plugin({
this.name, this.name,
this.path, this.path,
this.androidPackage, this.platforms,
this.iosPrefix,
this.macosPrefix,
this.pluginClass,
}); });
/// Parses [Plugin] specification from the provided pluginYaml.
///
/// This currently supports two formats. Legacy and Multi-platform.
/// Example of the deprecated Legacy format.
/// flutter:
/// plugin:
/// androidPackage: io.flutter.plugins.sample
/// iosPrefix: FLT
/// pluginClass: SamplePlugin
///
/// Example Multi-platform format.
/// flutter:
/// plugin:
/// platforms:
/// android:
/// package: io.flutter.plugins.sample
/// pluginClass: SamplePlugin
/// ios:
/// pluginClass: SamplePlugin
/// macos:
/// pluginClass: SamplePlugin
factory Plugin.fromYaml(String name, String path, dynamic pluginYaml) { factory Plugin.fromYaml(String name, String path, dynamic pluginYaml) {
String androidPackage; final List<String> errors = validatePluginYaml(pluginYaml);
String iosPrefix; if (errors.isNotEmpty) {
String macosPrefix; throwToolExit('Invalid plugin specification.\n${errors.join('\n')}');
String pluginClass; }
if (pluginYaml != null) { if (pluginYaml != null && pluginYaml['platforms'] != null) {
androidPackage = pluginYaml['androidPackage']; return Plugin._fromMultiPlatformYaml(name, path, pluginYaml);
iosPrefix = pluginYaml['iosPrefix'] ?? ''; } else {
// TODO(stuartmorgan): Add |?? ''| here as well once this isn't used as return Plugin._fromLegacyYaml(name, path, pluginYaml); // ignore: deprecated_member_use_from_same_package
// an indicator of macOS support, see https://github.com/flutter/flutter/issues/33597 }
macosPrefix = pluginYaml['macosPrefix']; }
pluginClass = pluginYaml['pluginClass'];
factory Plugin._fromMultiPlatformYaml(String name, String path, dynamic pluginYaml) {
assert (pluginYaml != null && pluginYaml['platforms'] != null,
'Invalid multi-platform plugin specification.');
final dynamic platformsYaml = pluginYaml['platforms'];
assert (_validateMultiPlatformYaml(platformsYaml).isEmpty,
'Invalid multi-platform plugin specification.');
final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
if (platformsYaml[AndroidPlugin.kConfigKey] != null) {
platforms[AndroidPlugin.kConfigKey] =
AndroidPlugin.fromYaml(name, platformsYaml[AndroidPlugin.kConfigKey]);
}
if (platformsYaml[IOSPlugin.kConfigKey] != null) {
platforms[IOSPlugin.kConfigKey] =
IOSPlugin.fromYaml(name, platformsYaml[IOSPlugin.kConfigKey]);
}
if (platformsYaml[MacOSPlugin.kConfigKey] != null) {
platforms[MacOSPlugin.kConfigKey] =
MacOSPlugin.fromYaml(name, platformsYaml[MacOSPlugin.kConfigKey]);
}
return Plugin(
name: name,
path: path,
platforms: platforms,
);
}
@deprecated
factory Plugin._fromLegacyYaml(String name, String path, dynamic pluginYaml) {
final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
final String pluginClass = pluginYaml['pluginClass'];
if (pluginYaml != null && pluginClass != null) {
final String androidPackage = pluginYaml['androidPackage'];
if (androidPackage != null) {
platforms[AndroidPlugin.kConfigKey] =
AndroidPlugin(
name: name,
package: pluginYaml['androidPackage'],
pluginClass: pluginClass,
);
}
final String iosPrefix = pluginYaml['iosPrefix'] ?? '';
platforms[IOSPlugin.kConfigKey] =
IOSPlugin(
name: name,
classPrefix: iosPrefix,
pluginClass: pluginClass,
);
} }
return Plugin( return Plugin(
name: name, name: name,
path: path, path: path,
androidPackage: androidPackage, platforms: platforms,
iosPrefix: iosPrefix,
macosPrefix: macosPrefix,
pluginClass: pluginClass,
); );
} }
static List<String> validatePluginYaml(YamlMap yaml) {
if (yaml.containsKey('platforms')) {
final int numKeys = yaml.keys.toSet().length;
if (numKeys != 1) {
return <String>[
'Invalid plugin specification. There must be only one key: "platforms", found multiple: ${yaml.keys.join(',')}'
];
} else {
return _validateMultiPlatformYaml(yaml['platforms']);
}
} else {
return _validateLegacyYaml(yaml);
}
}
static List<String> _validateMultiPlatformYaml(YamlMap yaml) {
final List<String> errors = <String>[];
if (yaml.containsKey(AndroidPlugin.kConfigKey) &&
!AndroidPlugin.validate(yaml[AndroidPlugin.kConfigKey])) {
errors.add('Invalid "android" plugin specification.');
}
if (yaml.containsKey(IOSPlugin.kConfigKey) &&
!IOSPlugin.validate(yaml[IOSPlugin.kConfigKey])) {
errors.add('Invalid "ios" plugin specification.');
}
if (yaml.containsKey(MacOSPlugin.kConfigKey) &&
!MacOSPlugin.validate(yaml[MacOSPlugin.kConfigKey])) {
errors.add('Invalid "macos" plugin specification.');
}
return errors;
}
static List<String> _validateLegacyYaml(YamlMap yaml) {
final List<String> errors = <String>[];
if (yaml['androidPackage'] != null && yaml['androidPackage'] is! String) {
errors.add('The "androidPackage" must either be null or a string.');
}
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..');
}
return errors;
}
final String name; final String name;
final String path; final String path;
final String androidPackage;
final String iosPrefix; /// This is a mapping from platform config key to the plugin platform spec.
final String macosPrefix; final Map<String, PluginPlatform> platforms;
final String pluginClass;
} }
Plugin _pluginFromPubspec(String name, Uri packageRoot) { Plugin _pluginFromPubspec(String name, Uri packageRoot) {
...@@ -153,15 +272,19 @@ public final class GeneratedPluginRegistrant { ...@@ -153,15 +272,19 @@ public final class GeneratedPluginRegistrant {
} }
'''; ''';
List<Map<String, dynamic>> _extractPlatformMaps(List<Plugin> plugins, String type) {
final List<Map<String, dynamic>> pluginConfigs = <Map<String, dynamic>>[];
for (Plugin p in plugins) {
final PluginPlatform platformPlugin = p.platforms[type];
if (platformPlugin != null) {
pluginConfigs.add(platformPlugin.toMap());
}
}
return pluginConfigs;
}
Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async { Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Map<String, dynamic>> androidPlugins = plugins final List<Map<String, dynamic>> androidPlugins = _extractPlatformMaps(plugins, AndroidPlugin.kConfigKey);
.where((Plugin p) => p.androidPackage != null && p.pluginClass != null)
.map<Map<String, dynamic>>((Plugin p) => <String, dynamic>{
'name': p.name,
'package': p.androidPackage,
'class': p.pluginClass,
})
.toList();
final Map<String, dynamic> context = <String, dynamic>{ final Map<String, dynamic> context = <String, dynamic>{
'plugins': androidPlugins, 'plugins': androidPlugins,
}; };
...@@ -262,13 +385,7 @@ end ...@@ -262,13 +385,7 @@ end
'''; ''';
Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async { Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Map<String, dynamic>> iosPlugins = plugins final List<Map<String, dynamic>> iosPlugins = _extractPlatformMaps(plugins, IOSPlugin.kConfigKey);
.where((Plugin p) => p.pluginClass != null)
.map<Map<String, dynamic>>((Plugin p) => <String, dynamic>{
'name': p.name,
'prefix': p.iosPrefix,
'class': p.pluginClass,
}).toList();
final Map<String, dynamic> context = <String, dynamic>{ final Map<String, dynamic> context = <String, dynamic>{
'os': 'ios', 'os': 'ios',
'deploymentTarget': '8.0', 'deploymentTarget': '8.0',
...@@ -308,14 +425,7 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug ...@@ -308,14 +425,7 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug
} }
Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async { Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
// TODO(stuartmorgan): Replace macosPrefix check with formal metadata check, final List<Map<String, dynamic>> macosPlugins = _extractPlatformMaps(plugins, MacOSPlugin.kConfigKey);
// see https://github.com/flutter/flutter/issues/33597.
final List<Map<String, dynamic>> macosPlugins = plugins
.where((Plugin p) => p.pluginClass != null && p.macosPrefix != null)
.map<Map<String, dynamic>>((Plugin p) => <String, dynamic>{
'name': p.name,
'class': p.pluginClass,
}).toList();
final Map<String, dynamic> context = <String, dynamic>{ final Map<String, dynamic> context = <String, dynamic>{
'os': 'macos', 'os': 'macos',
'framework': 'FlutterMacOS', 'framework': 'FlutterMacOS',
......
...@@ -57,6 +57,34 @@ ...@@ -57,6 +57,34 @@
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"platforms": {
"type": "object",
"additionalProperties": false,
"properties": {
"android": {
"type": "object",
"additionalProperties": false,
"properties": {
"package": {"type": "string"},
"pluginClass": {"type": "string"}
}
},
"ios": {
"type": "object",
"additionalProperties": false,
"properties": {
"pluginClass": {"type": "string"}
}
},
"macos": {
"type": "object",
"additionalProperties": false,
"properties": {
"pluginClass": {"type": "string"}
}
}
}
},
"androidPackage": { "type": "string" }, "androidPackage": { "type": "string" },
"iosPrefix": { "type": "string" }, "iosPrefix": { "type": "string" },
"macosPrefix": { "type": "string" }, "macosPrefix": { "type": "string" },
......
...@@ -41,9 +41,7 @@ def pubspec_supports_macos(file) ...@@ -41,9 +41,7 @@ def pubspec_supports_macos(file)
return false; return false;
end end
File.foreach(file_abs_path) { |line| File.foreach(file_abs_path) { |line|
# TODO(stuartmorgan): Use formal platform declaration once it exists, return true if line =~ /^\s*macos:/
# see https://github.com/flutter/flutter/issues/33597.
return true if line =~ /^\s*macosPrefix:/
} }
return false return false
end end
......
...@@ -374,7 +374,7 @@ flutter: ...@@ -374,7 +374,7 @@ flutter:
expect(flutterManifest.androidPackage, 'com.example'); expect(flutterManifest.androidPackage, 'com.example');
}); });
test('allows a plugin declaration', () async { test('allows a legacy plugin declaration', () async {
const String manifest = ''' const String manifest = '''
name: test name: test
flutter: flutter:
...@@ -385,6 +385,21 @@ flutter: ...@@ -385,6 +385,21 @@ flutter:
expect(flutterManifest.isPlugin, true); expect(flutterManifest.isPlugin, true);
expect(flutterManifest.androidPackage, 'com.example'); expect(flutterManifest.androidPackage, 'com.example');
}); });
test('allows a multi-plat plugin declaration', () async {
const String manifest = '''
name: test
flutter:
plugin:
platforms:
android:
package: com.example
pluginClass: TestPlugin
''';
final FlutterManifest flutterManifest = FlutterManifest.createFromString(manifest);
expect(flutterManifest.isPlugin, true);
expect(flutterManifest.androidPackage, 'com.example');
});
Future<void> checkManifestVersion({ Future<void> checkManifestVersion({
String manifest, String manifest,
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools/src/platform_plugins.dart';
import 'package:flutter_tools/src/plugins.dart';
import 'package:yaml/yaml.dart';
import '../src/common.dart';
const String _kTestPluginName = 'test_plugin_name';
const String _kTestPluginPath = 'test_plugin_path';
void main() {
group('PluginParsing', () {
test('Legacy Format', () {
const String pluginYamlRaw = 'androidPackage: com.flutter.dev\n'
'iosPrefix: FLT\n'
'pluginClass: SamplePlugin\n';
final dynamic pluginYaml = loadYaml(pluginYamlRaw);
final Plugin plugin =
Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml);
final AndroidPlugin androidPlugin =
plugin.platforms[AndroidPlugin.kConfigKey];
final IOSPlugin iosPlugin = plugin.platforms[IOSPlugin.kConfigKey];
final String androidPluginClass = androidPlugin.pluginClass;
final String iosPluginClass = iosPlugin.pluginClass;
expect(iosPluginClass, 'SamplePlugin');
expect(androidPluginClass, 'SamplePlugin');
expect(iosPlugin.classPrefix, 'FLT');
expect(androidPlugin.package, 'com.flutter.dev');
});
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';
final dynamic pluginYaml = loadYaml(pluginYamlRaw);
final Plugin plugin =
Plugin.fromYaml(_kTestPluginName, _kTestPluginPath, pluginYaml);
final AndroidPlugin androidPlugin =
plugin.platforms[AndroidPlugin.kConfigKey];
final MacOSPlugin macOSPlugin =
plugin.platforms[MacOSPlugin.kConfigKey];
final IOSPlugin iosPlugin = plugin.platforms[IOSPlugin.kConfigKey];
final String androidPluginClass = androidPlugin.pluginClass;
final String iosPluginClass = iosPlugin.pluginClass;
expect(iosPluginClass, 'ISamplePlugin');
expect(androidPluginClass, 'ASamplePlugin');
expect(iosPlugin.classPrefix, '');
expect(androidPlugin.package, 'com.flutter.dev');
expect(macOSPlugin.pluginClass, 'MSamplePlugin');
});
});
}
...@@ -231,10 +231,14 @@ void main() { ...@@ -231,10 +231,14 @@ void main() {
}); });
group('example', () { group('example', () {
testInMemory('exists for plugin', () async { testInMemory('exists for plugin in legacy format', () async {
final FlutterProject project = await aPluginProject(); final FlutterProject project = await aPluginProject();
expect(project.hasExampleApp, isTrue); expect(project.hasExampleApp, isTrue);
}); });
testInMemory('exists for plugin in multi-platform format', () async {
final FlutterProject project = await aPluginProject(legacy: false);
expect(project.hasExampleApp, isTrue);
});
testInMemory('does not exist for non-plugin', () async { testInMemory('does not exist for non-plugin', () async {
final FlutterProject project = await someProject(); final FlutterProject project = await someProject();
expect(project.hasExampleApp, isFalse); expect(project.hasExampleApp, isFalse);
...@@ -475,19 +479,37 @@ Future<FlutterProject> someProject() async { ...@@ -475,19 +479,37 @@ Future<FlutterProject> someProject() async {
return FlutterProject.fromDirectory(directory); return FlutterProject.fromDirectory(directory);
} }
Future<FlutterProject> aPluginProject() async { Future<FlutterProject> aPluginProject({bool legacy = true}) async {
final Directory directory = fs.directory('plugin_project'); final Directory directory = fs.directory('plugin_project');
directory.childDirectory('ios').createSync(recursive: true); directory.childDirectory('ios').createSync(recursive: true);
directory.childDirectory('android').createSync(recursive: true); directory.childDirectory('android').createSync(recursive: true);
directory.childDirectory('example').createSync(recursive: true); directory.childDirectory('example').createSync(recursive: true);
directory.childFile('pubspec.yaml').writeAsStringSync(''' String pluginPubSpec;
if (legacy) {
pluginPubSpec = '''
name: my_plugin name: my_plugin
flutter: flutter:
plugin: plugin:
androidPackage: com.example androidPackage: com.example
pluginClass: MyPlugin pluginClass: MyPlugin
iosPrefix: FLT iosPrefix: FLT
'''); ''';
} else {
pluginPubSpec = '''
name: my_plugin
flutter:
plugin:
platforms:
android:
package: com.example
pluginClass: MyPlugin
ios:
pluginClass: MyPlugin
macos:
pluginClass: MyPlugin
''';
}
directory.childFile('pubspec.yaml').writeAsStringSync(pluginPubSpec);
return FlutterProject.fromDirectory(directory); return FlutterProject.fromDirectory(directory);
} }
......
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