plugins.dart 14.5 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
  }) : assert(name != null),
       assert(path != null),
       assert(platforms != null),
26 27 28 29
       assert(defaultPackagePlatforms != null),
       assert(pluginDartClassPlatforms != null),
       assert(dependencies != null),
       assert(isDirectDependency != null);
30

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

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

    assert (_validateMultiPlatformYaml(platformsYaml).isEmpty,
112
            'Invalid multi-platform plugin specification $name.');
113 114 115

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

116
    if (_providesImplementationForPlatform(platformsYaml, AndroidPlugin.kConfigKey)) {
117 118
      platforms[AndroidPlugin.kConfigKey] = AndroidPlugin.fromYaml(
        name,
119
        platformsYaml[AndroidPlugin.kConfigKey] as YamlMap,
120
        path,
121
        fileSystem,
122
      );
123 124
    }

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

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

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

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

145
    if (_providesImplementationForPlatform(platformsYaml, WindowsPlugin.kConfigKey)) {
146
      platforms[WindowsPlugin.kConfigKey] =
147
          WindowsPlugin.fromYaml(name, platformsYaml[WindowsPlugin.kConfigKey] as YamlMap);
148 149
    }

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
    // 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;
        }
    }
172

173 174 175 176
    return Plugin(
      name: name,
      path: path,
      platforms: platforms,
177 178
      defaultPackagePlatforms: defaultPackages,
      pluginDartClassPlatforms: dartPluginClasses,
179
      flutterConstraint: flutterConstraint,
180
      dependencies: dependencies,
181 182
      isDirectDependency: isDirectDependency,
      implementsPackage: pluginYaml['implements'] != null ? pluginYaml['implements'] as String : '',
183 184 185
    );
  }

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

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

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

249
  static List<String> validatePluginYaml(YamlMap? yaml) {
250 251 252
    if (yaml == null) {
      return <String>['Invalid "plugin" specification.'];
    }
253 254 255 256 257 258 259 260 261 262 263

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

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

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

270 271 272 273 274 275 276 277
    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];
    }

278
    if (usesNewPluginFormat) {
279 280 281 282
      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];
      }
283
      return _validateMultiPlatformYaml(yaml['platforms'] as YamlMap?);
284 285 286 287 288
    } else {
      return _validateLegacyYaml(yaml);
    }
  }

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

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

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

329 330 331 332 333 334 335 336 337 338 339 340
    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;
  }

341
  static bool _supportsPlatform(YamlMap platformsYaml, String platformKey) {
342
    if (!platformsYaml.containsKey(platformKey)) {
343 344
      return false;
    }
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 370 371 372 373 374 375
    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)) {
376 377 378 379 380
      return false;
    }
    return true;
  }

381 382
  final String name;
  final String path;
383

384 385 386 387
  /// The name of the interface package that this plugin implements.
  /// If [null], this plugin doesn't implement an interface.
  final String? implementsPackage;

388 389 390
  /// The required version of Flutter, if specified.
  final VersionConstraint? flutterConstraint;

391 392 393
  /// The name of the packages this plugin depends on.
  final List<String> dependencies;

394 395
  /// This is a mapping from platform config key to the plugin platform spec.
  final Map<String, PluginPlatform> platforms;
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427

  /// 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,
  }) : assert(plugin != null),
       assert(platform != null);

  /// 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] ?? '',
    };
  }
428
}