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,
required this.isDevDependency,
this.implementsPackage,
});
factory Plugin.fromYaml(
String name,
String path,
YamlMap? pluginYaml,
VersionConstraint? flutterConstraint,
List<String> dependencies, {
required FileSystem fileSystem,
required bool isDevDependency,
Set<String>? appDependencies,
}) {
final List<String> errors = validatePluginYaml(pluginYaml);
if (errors.isNotEmpty) {
throwToolExit('Invalid plugin specification $name.\n${errors.join('\n')}');
}
if (pluginYaml?['platforms'] != null) {
return Plugin._fromMultiPlatformYaml(
name,
path,
pluginYaml!,
flutterConstraint,
dependencies,
fileSystem,
isDevDependency: isDevDependency,
appDependencies != null && appDependencies.contains(name),
);
}
return Plugin._fromLegacyYaml(
name,
path,
pluginYaml,
flutterConstraint,
dependencies,
fileSystem,
isDevDependency: isDevDependency,
appDependencies != null && appDependencies.contains(name),
);
}
factory Plugin._fromMultiPlatformYaml(
String name,
String path,
YamlMap pluginYaml,
VersionConstraint? flutterConstraint,
List<String> dependencies,
FileSystem fileSystem,
bool isDirectDependency, {
required bool isDevDependency,
}) {
final platformsYaml = pluginYaml['platforms'] as YamlMap;
assert(
_validateMultiPlatformYaml(parentMap: pluginYaml).isEmpty,
'Invalid multi-platform plugin specification $name.',
);
final 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,
);
}
final sharedHandlingPlatforms = <String>[
AndroidPlugin.kConfigKey,
IOSPlugin.kConfigKey,
LinuxPlugin.kConfigKey,
MacOSPlugin.kConfigKey,
WindowsPlugin.kConfigKey,
];
final defaultPackages = <String, String>{};
final dartPluginClasses = <String, DartPluginClassAndFilePair>{};
for (final platform in sharedHandlingPlatforms) {
final String? defaultPackage = _getDefaultPackageForPlatform(platformsYaml, platform);
if (defaultPackage != null) {
defaultPackages[platform] = defaultPackage;
}
final DartPluginClassAndFilePair? dartPair = _getPluginDartClassForPlatform(
platformsYaml,
platformKey: platform,
pluginName: name,
);
if (dartPair != null) {
dartPluginClasses[platform] = dartPair;
}
}
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 : '',
isDevDependency: isDevDependency,
);
}
factory Plugin._fromLegacyYaml(
String name,
String path,
dynamic pluginYaml,
VersionConstraint? flutterConstraint,
List<String> dependencies,
FileSystem fileSystem,
bool isDirectDependency, {
required bool isDevDependency,
}) {
final platforms = <String, PluginPlatform>{};
final pluginClass = (pluginYaml as Map<dynamic, dynamic>)['pluginClass'] as String?;
if (pluginClass != null) {
final 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, DartPluginClassAndFilePair>{},
flutterConstraint: flutterConstraint,
dependencies: dependencies,
isDirectDependency: isDirectDependency,
isDevDependency: isDevDependency,
);
}
static YamlMap createPlatformsYamlMap(
List<String> platforms,
String pluginClass,
String androidPackage,
) {
final map = <String, dynamic>{};
for (final 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 errorMessage =
'The flutter.plugin.platforms key cannot be used in combination with the old '
'flutter.plugin.{androidPackage,iosPrefix,pluginClass} keys. '
'See: https://flutter.dev/to/pubspec-plugin-platforms';
return <String>[errorMessage];
}
if (!usesOldPluginFormat && !usesNewPluginFormat) {
const 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/to/pubspec-plugin-platforms';
return <String>[errorMessage];
}
if (usesNewPluginFormat) {
return _validateMultiPlatformYaml(parentMap: yaml);
} else {
return _validateLegacyYaml(yaml);
}
}
static bool _hasNonStringValue(YamlMap yaml, String key) =>
yaml.containsKey(key) && yaml[key] is! String;
static bool _hasNonNullNonStringValue(YamlMap yaml, String key) =>
yaml[key] != null && yaml[key] is! String;
static List<String> _validateMultiPlatformYaml({required YamlMap parentMap}) {
final Object? platforms = parentMap['platforms'];
if (platforms is! YamlMap?) {
const errorMessage =
'flutter.plugin.platforms should be a map with the platform name as the key';
return <String>[errorMessage];
}
if (platforms == null) {
return <String>['Invalid "platforms" specification.'];
}
final YamlMap yaml = platforms;
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 (_hasNonStringValue(yamlValue, kDefaultPackage) ||
_hasNonStringValue(yamlValue, kDartPluginClass) ||
_hasNonNullNonStringValue(yamlValue, kDartFileName)) {
return true;
}
if (yamlValue.containsKey(kDefaultPackage)) {
return false;
}
return !validate(yamlValue);
}
return <String>[
if (isInvalid(AndroidPlugin.kConfigKey, AndroidPlugin.validate))
'Invalid "android" plugin specification.',
if (isInvalid(IOSPlugin.kConfigKey, IOSPlugin.validate))
'Invalid "ios" plugin specification.',
if (isInvalid(LinuxPlugin.kConfigKey, LinuxPlugin.validate))
'Invalid "linux" plugin specification.',
if (isInvalid(MacOSPlugin.kConfigKey, MacOSPlugin.validate))
'Invalid "macos" plugin specification.',
if (isInvalid(WindowsPlugin.kConfigKey, WindowsPlugin.validate))
'Invalid "windows" plugin specification.',
];
}
static List<String> _validateLegacyYaml(YamlMap yaml) {
final Object? androidPackage = yaml['androidPackage'];
final Object? iosPrefix = yaml['iosPrefix'];
final Object? pluginClass = yaml['pluginClass'];
const identifierRequirement =
'must be a valid identifier, optionally with dot-separated segments';
return <String>[
if (androidPackage is! String?)
'The "androidPackage" must either be null or a string.'
else if (androidPackage != null && !isValidPluginIdentifier(androidPackage))
'The "androidPackage" $identifierRequirement.',
if (iosPrefix is! String?)
'The "iosPrefix" must either be null or a string.'
else if (iosPrefix != null && iosPrefix.isNotEmpty && !isValidPluginIdentifier(iosPrefix))
'The "iosPrefix" $identifierRequirement.',
if (pluginClass is! String?)
'The "pluginClass" must either be null or a string.'
else if (pluginClass != null && !isValidPluginIdentifier(pluginClass))
'The "pluginClass" $identifierRequirement.',
];
}
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 DartPluginClassAndFilePair? _getPluginDartClassForPlatform(
YamlMap platformsYaml, {
required String platformKey,
required String pluginName,
}) {
if (!_supportsPlatform(platformsYaml, platformKey)) {
return null;
}
if ((platformsYaml[platformKey] as YamlMap).containsKey(kDartPluginClass)) {
final dartClass = (platformsYaml[platformKey] as YamlMap)[kDartPluginClass] as String;
final String dartFileName =
(platformsYaml[platformKey] as YamlMap)[kDartFileName] as String? ?? '$pluginName.dart';
if (!isValidPluginDartFileName(dartFileName)) {
throwToolExit(
'The plugin `$pluginName` has an invalid `dartFileName` for platform `$platformKey` '
'in pubspec.yaml.',
);
}
return (dartClass: dartClass, dartFileName: dartFileName);
}
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;
final String? implementsPackage;
final VersionConstraint? flutterConstraint;
final List<String> dependencies;
final Map<String, PluginPlatform> platforms;
final Map<String, String> defaultPackagePlatforms;
final Map<String, DartPluginClassAndFilePair> pluginDartClassPlatforms;
final bool isDirectDependency;
final bool isDevDependency;
String? pluginSwiftPackagePath(FileSystem fileSystem, String platform, {String? overridePath}) {
final String? platformDirectoryName = _darwinPluginDirectoryName(platform);
if (platformDirectoryName == null) {
return null;
}
if (overridePath != null) {
return fileSystem.path.join(overridePath, platformDirectoryName, name);
}
return fileSystem.path.join(path, platformDirectoryName, name);
}
String? pluginSwiftPackageManifestPath(FileSystem fileSystem, String platform) {
final String? packagePath = pluginSwiftPackagePath(fileSystem, platform);
if (packagePath == null) {
return null;
}
return fileSystem.path.join(packagePath, 'Package.swift');
}
bool supportSwiftPackageManagerForPlatform(FileSystem fileSystem, String platform) {
final String? manifestPath = pluginSwiftPackageManifestPath(fileSystem, platform);
return platforms[platform] != null &&
manifestPath != null &&
fileSystem.file(manifestPath).existsSync();
}
bool supportCocoapodsForPlatform(FileSystem fileSystem, String platform) {
final String? podspecPath = pluginPodspecPath(fileSystem, platform);
return platforms[platform] != null &&
podspecPath != null &&
fileSystem.file(podspecPath).existsSync();
}
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;
}
if (platformPlugin is DarwinPlugin && (platformPlugin as DarwinPlugin).sharedDarwinSource) {
return 'darwin';
}
return platform;
}
}
class PluginInterfaceResolution {
PluginInterfaceResolution({required this.plugin, required this.platform});
final Plugin plugin;
final String platform;
Map<String, String> toMap() {
return <String, String>{
'pluginName': plugin.name,
'platform': platform,
'dartClass': plugin.pluginDartClassPlatforms[platform]?.dartClass ?? '',
'dartFileName': plugin.pluginDartClassPlatforms[platform]?.dartFileName ?? '',
};
}
@override
String toString() {
return '<PluginInterfaceResolution ${plugin.name} for $platform>';
}
}
typedef DartPluginClassAndFilePair = ({String dartClass, String dartFileName});