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';
import 'base/utils.dart';
import 'cache.dart';
import 'globals.dart';
import 'plugins.dart';
/// A wrapper around the `flutter` section in the `pubspec.yaml` file.
class FlutterManifest {
......@@ -151,8 +152,14 @@ class FlutterManifest {
String get androidPackage {
if (isModule)
return _flutterDescriptor['module']['androidPackage'];
if (isPlugin)
return _flutterDescriptor['plugin']['androidPackage'];
if (isPlugin) {
final YamlMap plugin = _flutterDescriptor['plugin'];
if (plugin.containsKey('platforms')) {
return plugin['platforms']['android']['package'];
} else {
return plugin['androidPackage'];
}
}
return null;
}
......@@ -378,18 +385,8 @@ void _validateFlutter(YamlMap yaml, List<String> errors) {
if (kvp.value is! YamlMap) {
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) {
errors.add('The "androidPackage" must either be null or a string.');
}
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..');
}
final List<String> pluginErrors = Plugin.validatePluginYaml(kvp.value);
errors.addAll(pluginErrors);
break;
default:
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';
import 'package:mustache/mustache.dart' as mustache;
import 'package:yaml/yaml.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'dart/package_map.dart';
import 'features.dart';
import 'globals.dart';
import 'macos/cocoapods.dart';
import 'platform_plugins.dart';
import 'project.dart';
void _renderTemplateToFile(String template, dynamic context, String filePath) {
......@@ -26,41 +28,158 @@ class Plugin {
Plugin({
this.name,
this.path,
this.androidPackage,
this.iosPrefix,
this.macosPrefix,
this.pluginClass,
this.platforms,
});
/// 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) {
String androidPackage;
String iosPrefix;
String macosPrefix;
String pluginClass;
if (pluginYaml != null) {
androidPackage = pluginYaml['androidPackage'];
iosPrefix = pluginYaml['iosPrefix'] ?? '';
// TODO(stuartmorgan): Add |?? ''| here as well once this isn't used as
// an indicator of macOS support, see https://github.com/flutter/flutter/issues/33597
macosPrefix = pluginYaml['macosPrefix'];
pluginClass = pluginYaml['pluginClass'];
final List<String> errors = validatePluginYaml(pluginYaml);
if (errors.isNotEmpty) {
throwToolExit('Invalid plugin specification.\n${errors.join('\n')}');
}
if (pluginYaml != null && pluginYaml['platforms'] != null) {
return Plugin._fromMultiPlatformYaml(name, path, pluginYaml);
} else {
return Plugin._fromLegacyYaml(name, path, pluginYaml); // ignore: deprecated_member_use_from_same_package
}
}
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(
name: name,
path: path,
androidPackage: androidPackage,
iosPrefix: iosPrefix,
macosPrefix: macosPrefix,
pluginClass: pluginClass,
platforms: platforms,
);
}
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 path;
final String androidPackage;
final String iosPrefix;
final String macosPrefix;
final String pluginClass;
/// This is a mapping from platform config key to the plugin platform spec.
final Map<String, PluginPlatform> platforms;
}
Plugin _pluginFromPubspec(String name, Uri packageRoot) {
......@@ -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 {
final List<Map<String, dynamic>> androidPlugins = plugins
.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 List<Map<String, dynamic>> androidPlugins = _extractPlatformMaps(plugins, AndroidPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{
'plugins': androidPlugins,
};
......@@ -262,13 +385,7 @@ end
''';
Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Map<String, dynamic>> iosPlugins = plugins
.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 List<Map<String, dynamic>> iosPlugins = _extractPlatformMaps(plugins, IOSPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{
'os': 'ios',
'deploymentTarget': '8.0',
......@@ -308,14 +425,7 @@ Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plug
}
Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
// TODO(stuartmorgan): Replace macosPrefix check with formal metadata check,
// 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 List<Map<String, dynamic>> macosPlugins = _extractPlatformMaps(plugins, MacOSPlugin.kConfigKey);
final Map<String, dynamic> context = <String, dynamic>{
'os': 'macos',
'framework': 'FlutterMacOS',
......
......@@ -57,6 +57,34 @@
"type": "object",
"additionalProperties": false,
"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" },
"iosPrefix": { "type": "string" },
"macosPrefix": { "type": "string" },
......
......@@ -41,9 +41,7 @@ def pubspec_supports_macos(file)
return false;
end
File.foreach(file_abs_path) { |line|
# TODO(stuartmorgan): Use formal platform declaration once it exists,
# see https://github.com/flutter/flutter/issues/33597.
return true if line =~ /^\s*macosPrefix:/
return true if line =~ /^\s*macos:/
}
return false
end
......
......@@ -374,7 +374,7 @@ flutter:
expect(flutterManifest.androidPackage, 'com.example');
});
test('allows a plugin declaration', () async {
test('allows a legacy plugin declaration', () async {
const String manifest = '''
name: test
flutter:
......@@ -385,6 +385,21 @@ flutter:
expect(flutterManifest.isPlugin, true);
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({
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() {
});
group('example', () {
testInMemory('exists for plugin', () async {
testInMemory('exists for plugin in legacy format', () async {
final FlutterProject project = await aPluginProject();
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 {
final FlutterProject project = await someProject();
expect(project.hasExampleApp, isFalse);
......@@ -475,19 +479,37 @@ Future<FlutterProject> someProject() async {
return FlutterProject.fromDirectory(directory);
}
Future<FlutterProject> aPluginProject() async {
Future<FlutterProject> aPluginProject({bool legacy = true}) async {
final Directory directory = fs.directory('plugin_project');
directory.childDirectory('ios').createSync(recursive: true);
directory.childDirectory('android').createSync(recursive: true);
directory.childDirectory('example').createSync(recursive: true);
directory.childFile('pubspec.yaml').writeAsStringSync('''
String pluginPubSpec;
if (legacy) {
pluginPubSpec = '''
name: my_plugin
flutter:
plugin:
androidPackage: com.example
pluginClass: MyPlugin
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);
}
......
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