| // 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, | |
| required this.isDevDependency, | |
| 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 | |
| /// # Optional field to determine file containing dartPluginClass. | |
| /// # This file will be used in imports in generated files, e.g.: | |
| /// # import 'package:{{pluginName}}/{{dartFileName}}' | |
| /// # instead of default: | |
| /// # import 'package:{{pluginName}}/{{pluginName}}.dart' | |
| /// dartFileName: src/sample_plugin.dart | |
| /// 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, | |
| 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) { | |
| // SAFETY: Assumes that validatePluginYaml(pluginYaml) has been called. | |
| 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, | |
| }) { | |
| // SAFETY: This constructor is only invoked from .fromYaml, which validates. | |
| 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, | |
| ); | |
| } | |
| // 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 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, | |
| ); | |
| } | |
| /// 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>{}; | |
| 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 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 (yamlValue.containsKey('default_package')) { | |
| 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) { | |
| return <String>[ | |
| if (yaml['androidPackage'] is! String?) | |
| 'The "androidPackage" must either be null or a string.', | |
| if (yaml['iosPrefix'] is! String?) 'The "iosPrefix" must either be null or a string.', | |
| if (yaml['pluginClass'] is! String?) 'The "pluginClass" must either be null or a string.', | |
| ]; | |
| } | |
| 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'; | |
| 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; | |
| /// 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 Dart plugin class for the given platform. | |
| final Map<String, DartPluginClassAndFilePair> pluginDartClassPlatforms; | |
| /// Whether this plugin is a direct dependency of the app. | |
| /// If `false`, the plugin is a dependency of another plugin. | |
| final bool isDirectDependency; | |
| /// Whether this plugin is exclusively used as a dev dependency of the app. | |
| /// | |
| /// If `false`, the plugin is either: | |
| /// - _Not_ a dev dependency | |
| /// - _Not_ a dev dependency of some dependency that itself is not a dev | |
| /// dependency | |
| /// | |
| /// Dev dependencies are intended to be stripped out in release builds. | |
| final bool isDevDependency; | |
| /// Expected path to the plugin's swift package, which contains the Package.swift. | |
| /// | |
| /// This path should be `/path/to/[package_name]/[platform]/[package_name]` | |
| /// (e.g. `/path/to/my_plugin/ios/my_plugin`). | |
| /// | |
| /// Returns null if the plugin does not support the [platform] or the | |
| /// [platform] is not iOS or macOS. | |
| /// | |
| /// If [overridePath] is provided, returns `[overridePath]/[platform]/[package_name]`. | |
| 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); | |
| } | |
| /// 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? packagePath = pluginSwiftPackagePath(fileSystem, platform); | |
| if (packagePath == null) { | |
| return null; | |
| } | |
| return fileSystem.path.join(packagePath, 'Package.swift'); | |
| } | |
| /// Returns true if the plugin supports the [platform] and a Package.swift exists. | |
| bool supportSwiftPackageManagerForPlatform(FileSystem fileSystem, String platform) { | |
| final String? manifestPath = pluginSwiftPackageManifestPath(fileSystem, platform); | |
| return platforms[platform] != null && | |
| manifestPath != null && | |
| fileSystem.file(manifestPath).existsSync(); | |
| } | |
| /// Returns true if the plugin supports the [platform] and a podspec exists. | |
| bool supportCocoapodsForPlatform(FileSystem fileSystem, String platform) { | |
| final String? podspecPath = pluginPodspecPath(fileSystem, platform); | |
| return platforms[platform] != null && | |
| podspecPath != null && | |
| fileSystem.file(podspecPath).existsSync(); | |
| } | |
| /// 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]?.dartClass ?? '', | |
| 'dartFileName': plugin.pluginDartClassPlatforms[platform]?.dartFileName ?? '', | |
| }; | |
| } | |
| @override | |
| String toString() { | |
| return '<PluginInterfaceResolution ${plugin.name} for $platform>'; | |
| } | |
| } | |
| /// A record representing pair of dartPluginClass and dartFileName used as metadata | |
| /// in [PluginInterfaceResolution]. | |
| /// | |
| /// The `dartClass` and `dartFileName` fields are guaranteed to be non-null: | |
| /// | |
| /// - record should be created only if dartClassName exists in plugin configuration. | |
| /// - dartFileName either taken from configuration, or, if absent, should be | |
| /// constructed from plugin name. | |
| /// | |
| /// See also: | |
| /// - [PluginInterfaceResolution], which uses this record to create Map with metadata. | |
| typedef DartPluginClassAndFilePair = ({String dartClass, String dartFileName}); |