blob: d8e1c80f5c6dd6610aed6cec0f4b719fd14edabf [file] [log] [blame]
// Copyright 2014 The Flutter 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:pub_semver/pub_semver.dart';
import 'package:yaml/yaml.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'platform_plugins.dart';
class Plugin {
Plugin({
required this.name,
required this.path,
required this.platforms,
required this.defaultPackagePlatforms,
required this.pluginDartClassPlatforms,
this.flutterConstraint,
required this.dependencies,
required this.isDirectDependency,
this.implementsPackage,
});
/// 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:
/// # A plugin implemented through method channels.
/// pluginClass: SamplePlugin
/// linux:
/// # A plugin implemented purely in Dart code.
/// dartPluginClass: SamplePlugin
/// macos:
/// # A plugin implemented with `dart:ffi`.
/// ffiPlugin: true
/// windows:
/// # A plugin using platform-specific Dart and method channels.
/// dartPluginClass: SamplePlugin
/// pluginClass: SamplePlugin
factory Plugin.fromYaml(
String name,
String path,
YamlMap? pluginYaml,
VersionConstraint? flutterConstraint,
List<String> dependencies, {
required FileSystem fileSystem,
Set<String>? appDependencies,
}) {
final List<String> errors = validatePluginYaml(pluginYaml);
if (errors.isNotEmpty) {
throwToolExit('Invalid plugin specification $name.\n${errors.join('\n')}');
}
if (pluginYaml != null && pluginYaml['platforms'] != null) {
return Plugin._fromMultiPlatformYaml(
name,
path,
pluginYaml,
flutterConstraint,
dependencies,
fileSystem,
appDependencies != null && appDependencies.contains(name),
);
}
return Plugin._fromLegacyYaml(
name,
path,
pluginYaml,
flutterConstraint,
dependencies,
fileSystem,
appDependencies != null && appDependencies.contains(name),
);
}
factory Plugin._fromMultiPlatformYaml(
String name,
String path,
YamlMap pluginYaml,
VersionConstraint? flutterConstraint,
List<String> dependencies,
FileSystem fileSystem,
bool isDirectDependency,
) {
assert (pluginYaml['platforms'] != null, 'Invalid multi-platform plugin specification $name.');
final YamlMap platformsYaml = pluginYaml['platforms'] as YamlMap;
assert (_validateMultiPlatformYaml(platformsYaml).isEmpty,
'Invalid multi-platform plugin specification $name.');
final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
if (_providesImplementationForPlatform(platformsYaml, AndroidPlugin.kConfigKey)) {
platforms[AndroidPlugin.kConfigKey] = AndroidPlugin.fromYaml(
name,
platformsYaml[AndroidPlugin.kConfigKey] as YamlMap,
path,
fileSystem,
);
}
if (_providesImplementationForPlatform(platformsYaml, IOSPlugin.kConfigKey)) {
platforms[IOSPlugin.kConfigKey] =
IOSPlugin.fromYaml(name, platformsYaml[IOSPlugin.kConfigKey] as YamlMap);
}
if (_providesImplementationForPlatform(platformsYaml, LinuxPlugin.kConfigKey)) {
platforms[LinuxPlugin.kConfigKey] =
LinuxPlugin.fromYaml(name, platformsYaml[LinuxPlugin.kConfigKey] as YamlMap);
}
if (_providesImplementationForPlatform(platformsYaml, MacOSPlugin.kConfigKey)) {
platforms[MacOSPlugin.kConfigKey] =
MacOSPlugin.fromYaml(name, platformsYaml[MacOSPlugin.kConfigKey] as YamlMap);
}
if (_providesImplementationForPlatform(platformsYaml, WebPlugin.kConfigKey)) {
platforms[WebPlugin.kConfigKey] =
WebPlugin.fromYaml(name, platformsYaml[WebPlugin.kConfigKey] as YamlMap);
}
if (_providesImplementationForPlatform(platformsYaml, WindowsPlugin.kConfigKey)) {
platforms[WindowsPlugin.kConfigKey] =
WindowsPlugin.fromYaml(name, platformsYaml[WindowsPlugin.kConfigKey] as YamlMap);
}
// TODO(stuartmorgan): Consider merging web into this common handling; the
// fact that its implementation of Dart-only plugins and default packages
// are separate is legacy.
final List<String> sharedHandlingPlatforms = <String>[
AndroidPlugin.kConfigKey,
IOSPlugin.kConfigKey,
LinuxPlugin.kConfigKey,
MacOSPlugin.kConfigKey,
WindowsPlugin.kConfigKey,
];
final Map<String, String> defaultPackages = <String, String>{};
final Map<String, String> dartPluginClasses = <String, String>{};
for (final String platform in sharedHandlingPlatforms) {
final String? defaultPackage = _getDefaultPackageForPlatform(platformsYaml, platform);
if (defaultPackage != null) {
defaultPackages[platform] = defaultPackage;
}
final String? dartClass = _getPluginDartClassForPlatform(platformsYaml, platform);
if (dartClass != null) {
dartPluginClasses[platform] = dartClass;
}
}
return Plugin(
name: name,
path: path,
platforms: platforms,
defaultPackagePlatforms: defaultPackages,
pluginDartClassPlatforms: dartPluginClasses,
flutterConstraint: flutterConstraint,
dependencies: dependencies,
isDirectDependency: isDirectDependency,
implementsPackage: pluginYaml['implements'] != null ? pluginYaml['implements'] as String : '',
);
}
factory Plugin._fromLegacyYaml(
String name,
String path,
dynamic pluginYaml,
VersionConstraint? flutterConstraint,
List<String> dependencies,
FileSystem fileSystem,
bool isDirectDependency,
) {
final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
final String? pluginClass = (pluginYaml as Map<dynamic, dynamic>)['pluginClass'] as String?;
if (pluginClass != null) {
final String? androidPackage = pluginYaml['androidPackage'] as String?;
if (androidPackage != null) {
platforms[AndroidPlugin.kConfigKey] = AndroidPlugin(
name: name,
package: androidPackage,
pluginClass: pluginClass,
pluginPath: path,
fileSystem: fileSystem,
);
}
final String iosPrefix = pluginYaml['iosPrefix'] as String? ?? '';
platforms[IOSPlugin.kConfigKey] =
IOSPlugin(
name: name,
classPrefix: iosPrefix,
pluginClass: pluginClass,
);
}
return Plugin(
name: name,
path: path,
platforms: platforms,
defaultPackagePlatforms: <String, String>{},
pluginDartClassPlatforms: <String, String>{},
flutterConstraint: flutterConstraint,
dependencies: dependencies,
isDirectDependency: isDirectDependency,
);
}
/// Create a YamlMap that represents the supported platforms.
///
/// For example, if the `platforms` contains 'ios' and 'android', the return map looks like:
///
/// android:
/// package: io.flutter.plugins.sample
/// pluginClass: SamplePlugin
/// ios:
/// pluginClass: SamplePlugin
static YamlMap createPlatformsYamlMap(List<String> platforms, String pluginClass, String androidPackage) {
final Map<String, dynamic> map = <String, dynamic>{};
for (final String platform in platforms) {
map[platform] = <String, String>{
'pluginClass': pluginClass,
...platform == 'android' ? <String, String>{'package': androidPackage} : <String, String>{},
};
}
return YamlMap.wrap(map);
}
static List<String> validatePluginYaml(YamlMap? yaml) {
if (yaml == null) {
return <String>['Invalid "plugin" specification.'];
}
final bool usesOldPluginFormat = const <String>{
'androidPackage',
'iosPrefix',
'pluginClass',
}.any(yaml.containsKey);
final bool usesNewPluginFormat = yaml.containsKey('platforms');
if (usesOldPluginFormat && usesNewPluginFormat) {
const String errorMessage =
'The flutter.plugin.platforms key cannot be used in combination with the old '
'flutter.plugin.{androidPackage,iosPrefix,pluginClass} keys. '
'See: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin';
return <String>[errorMessage];
}
if (!usesOldPluginFormat && !usesNewPluginFormat) {
const String errorMessage =
'Cannot find the `flutter.plugin.platforms` key in the `pubspec.yaml` file. '
'An instruction to format the `pubspec.yaml` can be found here: '
'https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms';
return <String>[errorMessage];
}
if (usesNewPluginFormat) {
if (yaml['platforms'] != null && yaml['platforms'] is! YamlMap) {
const String errorMessage = 'flutter.plugin.platforms should be a map with the platform name as the key';
return <String>[errorMessage];
}
return _validateMultiPlatformYaml(yaml['platforms'] as YamlMap?);
} else {
return _validateLegacyYaml(yaml);
}
}
static List<String> _validateMultiPlatformYaml(YamlMap? yaml) {
bool isInvalid(String key, bool Function(YamlMap) validate) {
if (!yaml!.containsKey(key)) {
return false;
}
final dynamic yamlValue = yaml[key];
if (yamlValue is! YamlMap) {
return true;
}
if (yamlValue.containsKey('default_package')) {
return false;
}
return !validate(yamlValue);
}
if (yaml == null) {
return <String>['Invalid "platforms" specification.'];
}
final List<String> errors = <String>[];
if (isInvalid(AndroidPlugin.kConfigKey, AndroidPlugin.validate)) {
errors.add('Invalid "android" plugin specification.');
}
if (isInvalid(IOSPlugin.kConfigKey, IOSPlugin.validate)) {
errors.add('Invalid "ios" plugin specification.');
}
if (isInvalid(LinuxPlugin.kConfigKey, LinuxPlugin.validate)) {
errors.add('Invalid "linux" plugin specification.');
}
if (isInvalid(MacOSPlugin.kConfigKey, MacOSPlugin.validate)) {
errors.add('Invalid "macos" plugin specification.');
}
if (isInvalid(WindowsPlugin.kConfigKey, WindowsPlugin.validate)) {
errors.add('Invalid "windows" 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['pluginClass'] != null && yaml['pluginClass'] is! String) {
errors.add('The "pluginClass" must either be null or a string..');
}
return errors;
}
static bool _supportsPlatform(YamlMap platformsYaml, String platformKey) {
if (!platformsYaml.containsKey(platformKey)) {
return false;
}
if (platformsYaml[platformKey] is YamlMap) {
return true;
}
return false;
}
static String? _getDefaultPackageForPlatform(YamlMap platformsYaml, String platformKey) {
if (!_supportsPlatform(platformsYaml, platformKey)) {
return null;
}
if ((platformsYaml[platformKey] as YamlMap).containsKey(kDefaultPackage)) {
return (platformsYaml[platformKey] as YamlMap)[kDefaultPackage] as String;
}
return null;
}
static String? _getPluginDartClassForPlatform(YamlMap platformsYaml, String platformKey) {
if (!_supportsPlatform(platformsYaml, platformKey)) {
return null;
}
if ((platformsYaml[platformKey] as YamlMap).containsKey(kDartPluginClass)) {
return (platformsYaml[platformKey] as YamlMap)[kDartPluginClass] as String;
}
return null;
}
static bool _providesImplementationForPlatform(YamlMap platformsYaml, String platformKey) {
if (!_supportsPlatform(platformsYaml, platformKey)) {
return false;
}
if ((platformsYaml[platformKey] as YamlMap).containsKey(kDefaultPackage)) {
return false;
}
return true;
}
final String name;
final String path;
/// The name of the interface package that this plugin implements.
/// If [null], this plugin doesn't implement an interface.
final String? implementsPackage;
/// The required version of Flutter, if specified.
final VersionConstraint? flutterConstraint;
/// The name of the packages this plugin depends on.
final List<String> dependencies;
/// This is a mapping from platform config key to the plugin platform spec.
final Map<String, PluginPlatform> platforms;
/// This is a mapping from platform config key to the default package implementation.
final Map<String, String> defaultPackagePlatforms;
/// This is a mapping from platform config key to the plugin class for the given platform.
final Map<String, String> pluginDartClassPlatforms;
/// Whether this plugin is a direct dependency of the app.
/// If [false], the plugin is a dependency of another plugin.
final bool isDirectDependency;
/// Expected path to the plugin's Package.swift. Returns null if the plugin
/// does not support the [platform] or the [platform] is not iOS or macOS.
String? pluginSwiftPackageManifestPath(
FileSystem fileSystem,
String platform,
) {
final String? platformDirectoryName = _darwinPluginDirectoryName(platform);
if (platformDirectoryName == null) {
return null;
}
return fileSystem.path.join(
path,
platformDirectoryName,
name,
'Package.swift',
);
}
/// Expected path to the plugin's podspec. Returns null if the plugin does
/// not support the [platform] or the [platform] is not iOS or macOS.
String? pluginPodspecPath(FileSystem fileSystem, String platform) {
final String? platformDirectoryName = _darwinPluginDirectoryName(platform);
if (platformDirectoryName == null) {
return null;
}
return fileSystem.path.join(path, platformDirectoryName, '$name.podspec');
}
String? _darwinPluginDirectoryName(String platform) {
final PluginPlatform? platformPlugin = platforms[platform];
if (platformPlugin == null ||
(platform != IOSPlugin.kConfigKey &&
platform != MacOSPlugin.kConfigKey)) {
return null;
}
// iOS and macOS code can be shared in "darwin" directory, otherwise
// respectively in "ios" or "macos" directories.
if (platformPlugin is DarwinPlugin &&
(platformPlugin as DarwinPlugin).sharedDarwinSource) {
return 'darwin';
}
return platform;
}
}
/// Metadata associated with the resolution of a platform interface of a plugin.
class PluginInterfaceResolution {
PluginInterfaceResolution({
required this.plugin,
required this.platform,
});
/// The plugin.
final Plugin plugin;
// The name of the platform that this plugin implements.
final String platform;
Map<String, String> toMap() {
return <String, String> {
'pluginName': plugin.name,
'platform': platform,
'dartClass': plugin.pluginDartClassPlatforms[platform] ?? '',
};
}
@override
String toString() {
return '<PluginInterfaceResolution ${plugin.name} for $platform>';
}
}