| // 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: pluginYaml['androidPackage'] as String, |
| 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] ?? '', |
| }; |
| } |
| } |