// 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; } /// 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>'; } }