plugins.dart 14.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:pub_semver/pub_semver.dart';
6 7
import 'package:yaml/yaml.dart';

8
import 'base/common.dart';
9
import 'base/file_system.dart';
10
import 'platform_plugins.dart';
11

12
class Plugin {
13
  Plugin({
14 15 16
    required this.name,
    required this.path,
    required this.platforms,
17 18
    required this.defaultPackagePlatforms,
    required this.pluginDartClassPlatforms,
19
    this.flutterConstraint,
20
    required this.dependencies,
21 22
    required this.isDirectDependency,
    this.implementsPackage,
23
  });
24

25 26 27
  /// Parses [Plugin] specification from the provided pluginYaml.
  ///
  /// This currently supports two formats. Legacy and Multi-platform.
28
  ///
29 30
  /// Example of the deprecated Legacy format.
  ///
31 32 33 34
  ///     flutter:
  ///      plugin:
  ///        androidPackage: io.flutter.plugins.sample
  ///        iosPrefix: FLT
35
  ///        pluginClass: SamplePlugin
36 37 38 39 40 41 42 43 44 45
  ///
  /// Example Multi-platform format.
  ///
  ///     flutter:
  ///      plugin:
  ///        platforms:
  ///          android:
  ///            package: io.flutter.plugins.sample
  ///            pluginClass: SamplePlugin
  ///          ios:
Daco Harkes's avatar
Daco Harkes committed
46
  ///            # A plugin implemented through method channels.
47 48
  ///            pluginClass: SamplePlugin
  ///          linux:
Daco Harkes's avatar
Daco Harkes committed
49 50
  ///            # A plugin implemented purely in Dart code.
  ///            dartPluginClass: SamplePlugin
51
  ///          macos:
Daco Harkes's avatar
Daco Harkes committed
52 53
  ///            # A plugin implemented with `dart:ffi`.
  ///            ffiPlugin: true
54
  ///          windows:
Daco Harkes's avatar
Daco Harkes committed
55 56
  ///            # A plugin using platform-specific Dart and method channels.
  ///            dartPluginClass: SamplePlugin
57
  ///            pluginClass: SamplePlugin
58 59 60
  factory Plugin.fromYaml(
    String name,
    String path,
61
    YamlMap? pluginYaml,
62
    VersionConstraint? flutterConstraint,
63
    List<String> dependencies, {
64
    required FileSystem fileSystem,
65
    Set<String>? appDependencies,
66
  }) {
67 68
    final List<String> errors = validatePluginYaml(pluginYaml);
    if (errors.isNotEmpty) {
69
      throwToolExit('Invalid plugin specification $name.\n${errors.join('\n')}');
70 71
    }
    if (pluginYaml != null && pluginYaml['platforms'] != null) {
72 73 74 75
      return Plugin._fromMultiPlatformYaml(
        name,
        path,
        pluginYaml,
76
        flutterConstraint,
77 78 79 80
        dependencies,
        fileSystem,
        appDependencies != null && appDependencies.contains(name),
      );
81
    }
82 83 84 85
    return Plugin._fromLegacyYaml(
      name,
      path,
      pluginYaml,
86
      flutterConstraint,
87 88 89 90
      dependencies,
      fileSystem,
      appDependencies != null && appDependencies.contains(name),
    );
91 92
  }

93 94 95
  factory Plugin._fromMultiPlatformYaml(
    String name,
    String path,
96
    YamlMap pluginYaml,
97
    VersionConstraint? flutterConstraint,
98
    List<String> dependencies,
99
    FileSystem fileSystem,
100
    bool isDirectDependency,
101
  ) {
102
    assert (pluginYaml['platforms'] != null, 'Invalid multi-platform plugin specification $name.');
103
    final YamlMap platformsYaml = pluginYaml['platforms'] as YamlMap;
104 105

    assert (_validateMultiPlatformYaml(platformsYaml).isEmpty,
106
            'Invalid multi-platform plugin specification $name.');
107 108 109

    final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};

110
    if (_providesImplementationForPlatform(platformsYaml, AndroidPlugin.kConfigKey)) {
111 112
      platforms[AndroidPlugin.kConfigKey] = AndroidPlugin.fromYaml(
        name,
113
        platformsYaml[AndroidPlugin.kConfigKey] as YamlMap,
114
        path,
115
        fileSystem,
116
      );
117 118
    }

119
    if (_providesImplementationForPlatform(platformsYaml, IOSPlugin.kConfigKey)) {
120
      platforms[IOSPlugin.kConfigKey] =
121
          IOSPlugin.fromYaml(name, platformsYaml[IOSPlugin.kConfigKey] as YamlMap);
122 123
    }

124
    if (_providesImplementationForPlatform(platformsYaml, LinuxPlugin.kConfigKey)) {
125
      platforms[LinuxPlugin.kConfigKey] =
126
          LinuxPlugin.fromYaml(name, platformsYaml[LinuxPlugin.kConfigKey] as YamlMap);
127 128
    }

129
    if (_providesImplementationForPlatform(platformsYaml, MacOSPlugin.kConfigKey)) {
130
      platforms[MacOSPlugin.kConfigKey] =
131
          MacOSPlugin.fromYaml(name, platformsYaml[MacOSPlugin.kConfigKey] as YamlMap);
132 133
    }

134
    if (_providesImplementationForPlatform(platformsYaml, WebPlugin.kConfigKey)) {
135
      platforms[WebPlugin.kConfigKey] =
136
          WebPlugin.fromYaml(name, platformsYaml[WebPlugin.kConfigKey] as YamlMap);
137 138
    }

139
    if (_providesImplementationForPlatform(platformsYaml, WindowsPlugin.kConfigKey)) {
140
      platforms[WindowsPlugin.kConfigKey] =
141
          WindowsPlugin.fromYaml(name, platformsYaml[WindowsPlugin.kConfigKey] as YamlMap);
142 143
    }

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
    // 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;
        }
    }
166

167 168 169 170
    return Plugin(
      name: name,
      path: path,
      platforms: platforms,
171 172
      defaultPackagePlatforms: defaultPackages,
      pluginDartClassPlatforms: dartPluginClasses,
173
      flutterConstraint: flutterConstraint,
174
      dependencies: dependencies,
175 176
      isDirectDependency: isDirectDependency,
      implementsPackage: pluginYaml['implements'] != null ? pluginYaml['implements'] as String : '',
177 178 179
    );
  }

180 181 182 183
  factory Plugin._fromLegacyYaml(
    String name,
    String path,
    dynamic pluginYaml,
184
    VersionConstraint? flutterConstraint,
185
    List<String> dependencies,
186
    FileSystem fileSystem,
187
    bool isDirectDependency,
188
  ) {
189
    final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
190 191 192
    final String? pluginClass = (pluginYaml as Map<dynamic, dynamic>)['pluginClass'] as String?;
    if (pluginClass != null) {
      final String? androidPackage = pluginYaml['androidPackage'] as String?;
193
      if (androidPackage != null) {
194 195
        platforms[AndroidPlugin.kConfigKey] = AndroidPlugin(
          name: name,
196
          package: androidPackage,
197 198
          pluginClass: pluginClass,
          pluginPath: path,
199
          fileSystem: fileSystem,
200
        );
201 202
      }

203
      final String iosPrefix = pluginYaml['iosPrefix'] as String? ?? '';
204 205 206 207 208 209
      platforms[IOSPlugin.kConfigKey] =
          IOSPlugin(
            name: name,
            classPrefix: iosPrefix,
            pluginClass: pluginClass,
          );
210
    }
211
    return Plugin(
212 213
      name: name,
      path: path,
214
      platforms: platforms,
215 216
      defaultPackagePlatforms: <String, String>{},
      pluginDartClassPlatforms: <String, String>{},
217
      flutterConstraint: flutterConstraint,
218
      dependencies: dependencies,
219
      isDirectDependency: isDirectDependency,
220
    );
221
  }
222

223 224 225
  /// Create a YamlMap that represents the supported platforms.
  ///
  /// For example, if the `platforms` contains 'ios' and 'android', the return map looks like:
226 227 228 229 230 231
  ///
  ///     android:
  ///       package: io.flutter.plugins.sample
  ///       pluginClass: SamplePlugin
  ///     ios:
  ///       pluginClass: SamplePlugin
232 233 234 235 236 237 238 239 240 241 242
  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);
  }

243
  static List<String> validatePluginYaml(YamlMap? yaml) {
244 245 246
    if (yaml == null) {
      return <String>['Invalid "plugin" specification.'];
    }
247 248 249 250 251 252 253 254 255 256 257

    final bool usesOldPluginFormat = const <String>{
      'androidPackage',
      'iosPrefix',
      'pluginClass',
    }.any(yaml.containsKey);

    final bool usesNewPluginFormat = yaml.containsKey('platforms');

    if (usesOldPluginFormat && usesNewPluginFormat) {
      const String errorMessage =
258 259
          'The flutter.plugin.platforms key cannot be used in combination with the old '
          'flutter.plugin.{androidPackage,iosPrefix,pluginClass} keys. '
260 261 262 263
          'See: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin';
      return <String>[errorMessage];
    }

264 265 266 267 268 269 270 271
    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];
    }

272
    if (usesNewPluginFormat) {
273 274 275 276
      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];
      }
277
      return _validateMultiPlatformYaml(yaml['platforms'] as YamlMap?);
278 279 280 281 282
    } else {
      return _validateLegacyYaml(yaml);
    }
  }

283
  static List<String> _validateMultiPlatformYaml(YamlMap? yaml) {
284
    bool isInvalid(String key, bool Function(YamlMap) validate) {
285
      if (!yaml!.containsKey(key)) {
286 287
        return false;
      }
288 289
      final dynamic yamlValue = yaml[key];
      if (yamlValue is! YamlMap) {
290
        return true;
291
      }
292
      if (yamlValue.containsKey('default_package')) {
293 294
        return false;
      }
295
      return !validate(yamlValue);
296
    }
297 298 299 300

    if (yaml == null) {
      return <String>['Invalid "platforms" specification.'];
    }
301
    final List<String> errors = <String>[];
302
    if (isInvalid(AndroidPlugin.kConfigKey, AndroidPlugin.validate)) {
303 304
      errors.add('Invalid "android" plugin specification.');
    }
305
    if (isInvalid(IOSPlugin.kConfigKey, IOSPlugin.validate)) {
306 307
      errors.add('Invalid "ios" plugin specification.');
    }
308
    if (isInvalid(LinuxPlugin.kConfigKey, LinuxPlugin.validate)) {
309 310
      errors.add('Invalid "linux" plugin specification.');
    }
311
    if (isInvalid(MacOSPlugin.kConfigKey, MacOSPlugin.validate)) {
312 313
      errors.add('Invalid "macos" plugin specification.');
    }
314
    if (isInvalid(WindowsPlugin.kConfigKey, WindowsPlugin.validate)) {
315 316
      errors.add('Invalid "windows" plugin specification.');
    }
317 318 319 320 321
    return errors;
  }

  static List<String> _validateLegacyYaml(YamlMap yaml) {
    final List<String> errors = <String>[];
322

323 324 325 326 327 328 329 330 331 332 333 334
    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;
  }

335
  static bool _supportsPlatform(YamlMap platformsYaml, String platformKey) {
336
    if (!platformsYaml.containsKey(platformKey)) {
337 338
      return false;
    }
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
    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)) {
370 371 372 373 374
      return false;
    }
    return true;
  }

375 376
  final String name;
  final String path;
377

378 379 380 381
  /// The name of the interface package that this plugin implements.
  /// If [null], this plugin doesn't implement an interface.
  final String? implementsPackage;

382 383 384
  /// The required version of Flutter, if specified.
  final VersionConstraint? flutterConstraint;

385 386 387
  /// The name of the packages this plugin depends on.
  final List<String> dependencies;

388 389
  /// This is a mapping from platform config key to the plugin platform spec.
  final Map<String, PluginPlatform> platforms;
390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406

  /// 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,
407
  });
408 409 410 411 412 413 414 415 416 417 418 419 420

  /// 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] ?? '',
    };
  }
421 422 423 424 425

  @override
  String toString() {
    return '<PluginInterfaceResolution ${plugin.name} for $platform>';
  }
426
}