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

import 'package:yaml/yaml.dart';

7 8 9
import 'base/common.dart';
import 'base/file_system.dart';

10 11 12
/// Constant for 'pluginClass' key in plugin maps.
const String kPluginClass = 'pluginClass';

Daco Harkes's avatar
Daco Harkes committed
13
/// Constant for 'dartPluginClass' key in plugin maps.
14 15
const String kDartPluginClass = 'dartPluginClass';

Daco Harkes's avatar
Daco Harkes committed
16 17 18
/// Constant for 'ffiPlugin' key in plugin maps.
const String kFfiPlugin = 'ffiPlugin';

19 20 21
// Constant for 'defaultPackage' key in plugin maps.
const String kDefaultPackage = 'default_package';

22 23 24 25 26 27 28 29 30
/// Constant for 'supportedVariants' key in plugin maps.
const String kSupportedVariants = 'supportedVariants';

/// Platform variants that a Windows plugin can support.
enum PluginPlatformVariant {
  /// Win32 variant of Windows.
  win32,
}

31
/// Marker interface for all platform specific plugin config implementations.
32 33 34 35 36 37
abstract class PluginPlatform {
  const PluginPlatform();

  Map<String, dynamic> toMap();
}

38 39 40 41 42 43
/// A plugin that has platform variants.
abstract class VariantPlatformPlugin {
  /// The platform variants supported by the plugin.
  Set<PluginPlatformVariant> get supportedVariants;
}

44
abstract class NativeOrDartPlugin {
Daco Harkes's avatar
Daco Harkes committed
45 46 47 48 49 50 51 52
  /// Determines whether the plugin has a Dart implementation.
  bool hasDart();

  /// Determines whether the plugin has a FFI implementation.
  bool hasFfi();

  /// Determines whether the plugin has a method channel implementation.
  bool hasMethodChannel();
53 54
}

55 56
/// Contains parameters to template an Android plugin.
///
57 58 59 60 61 62 63 64 65
/// The [name] of the plugin is required. Additionally, either:
/// - [defaultPackage], or
/// - an implementation consisting of:
///   - the [package] and [pluginClass] that will be the entry point to the
///     plugin's native code, and/or
///   - the [dartPluginClass] that will be the entry point for the plugin's
///     Dart code
/// is required.
class AndroidPlugin extends PluginPlatform implements NativeOrDartPlugin {
66
  AndroidPlugin({
67 68
    required this.name,
    required this.pluginPath,
69 70 71
    this.package,
    this.pluginClass,
    this.dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
72
    bool? ffiPlugin,
73
    this.defaultPackage,
74
    required FileSystem fileSystem,
Daco Harkes's avatar
Daco Harkes committed
75 76
  })  : _fileSystem = fileSystem,
        ffiPlugin = ffiPlugin ?? false;
77

78
  factory AndroidPlugin.fromYaml(String name, YamlMap yaml, String pluginPath, FileSystem fileSystem) {
79 80 81
    assert(validate(yaml));
    return AndroidPlugin(
      name: name,
82 83 84
      package: yaml['package'] as String?,
      pluginClass: yaml[kPluginClass] as String?,
      dartPluginClass: yaml[kDartPluginClass] as String?,
Daco Harkes's avatar
Daco Harkes committed
85
      ffiPlugin: yaml[kFfiPlugin] as bool?,
86
      defaultPackage: yaml[kDefaultPackage] as String?,
87
      pluginPath: pluginPath,
88
      fileSystem: fileSystem,
89 90 91
    );
  }

92 93
  final FileSystem _fileSystem;

94
  @override
Daco Harkes's avatar
Daco Harkes committed
95 96 97 98 99 100 101
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;
102

103 104 105 106
  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
Daco Harkes's avatar
Daco Harkes committed
107 108 109 110
    return (yaml['package'] is String && yaml[kPluginClass] is String) ||
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kDefaultPackage] is String;
111 112 113 114
  }

  static const String kConfigKey = 'android';

115
  /// The plugin name defined in pubspec.yaml.
116
  final String name;
117 118

  /// The plugin package name defined in pubspec.yaml.
119 120 121 122 123 124 125
  final String? package;

  /// The native plugin main class defined in pubspec.yaml, if any.
  final String? pluginClass;

  /// The Dart plugin main class defined in pubspec.yaml, if any.
  final String? dartPluginClass;
126

Daco Harkes's avatar
Daco Harkes committed
127 128 129
  /// Is FFI plugin defined in pubspec.yaml.
  final bool ffiPlugin;

130 131
  /// The default implementation package defined in pubspec.yaml, if any.
  final String? defaultPackage;
132

133 134 135
  /// The absolute path to the plugin in the pub cache.
  final String pluginPath;

136 137 138 139
  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
140 141 142
      if (package != null) 'package': package,
      if (pluginClass != null) 'class': pluginClass,
      if (dartPluginClass != null) kDartPluginClass : dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
143
      if (ffiPlugin) kFfiPlugin: true,
144
      if (defaultPackage != null) kDefaultPackage : defaultPackage,
145
      // Mustache doesn't support complex types.
146 147
      'supportsEmbeddingV1': _supportedEmbeddings.contains('1'),
      'supportsEmbeddingV2': _supportedEmbeddings.contains('2'),
148 149
    };
  }
150 151

  /// Returns the version of the Android embedding.
152
  late final Set<String> _supportedEmbeddings = _getSupportedEmbeddings();
153

154
  Set<String> _getSupportedEmbeddings() {
155
    assert(pluginPath != null);
156
    final Set<String> supportedEmbeddings = <String>{};
157
    final String baseMainPath = _fileSystem.path.join(
158 159 160 161 162
      pluginPath,
      'android',
      'src',
      'main',
    );
163

164 165 166 167 168 169 170
    final String? package = this.package;
    // Don't attempt to validate the native code if there isn't supposed to
    // be any.
    if (package == null) {
      return supportedEmbeddings;
    }

171
    final List<String> mainClassCandidates = <String>[
172
      _fileSystem.path.join(
173 174
        baseMainPath,
        'java',
175
        package.replaceAll('.', _fileSystem.path.separator),
176
        '$pluginClass.java',
177
      ),
178
      _fileSystem.path.join(
179 180
        baseMainPath,
        'kotlin',
181
        package.replaceAll('.', _fileSystem.path.separator),
182
        '$pluginClass.kt',
183
      ),
184 185
    ];

186
    File? mainPluginClass;
187 188
    bool mainClassFound = false;
    for (final String mainClassCandidate in mainClassCandidates) {
189
      mainPluginClass = _fileSystem.file(mainClassCandidate);
190 191 192 193 194
      if (mainPluginClass.existsSync()) {
        mainClassFound = true;
        break;
      }
    }
195
    if (mainPluginClass == null || !mainClassFound) {
196 197
      assert(mainClassCandidates.length <= 2);
      throwToolExit(
198 199
        "The plugin `$name` doesn't have a main class defined in ${mainClassCandidates.join(' or ')}. "
        "This is likely to due to an incorrect `androidPackage: $package` or `mainClass` entry in the plugin's pubspec.yaml.\n"
200 201
        'If you are the author of this plugin, fix the `androidPackage` entry or move the main class to any of locations used above. '
        'Otherwise, please contact the author of this plugin and consider using a different plugin in the meanwhile. '
202 203
      );
    }
204

205
    final String mainClassContent = mainPluginClass.readAsStringSync();
206 207
    if (mainClassContent
        .contains('io.flutter.embedding.engine.plugins.FlutterPlugin')) {
208 209 210
      supportedEmbeddings.add('2');
    } else {
      supportedEmbeddings.add('1');
211
    }
212 213 214 215 216
    if (mainClassContent.contains('PluginRegistry')
        && mainClassContent.contains('registerWith')) {
      supportedEmbeddings.add('1');
    }
    return supportedEmbeddings;
217
  }
218 219 220 221
}

/// Contains the parameters to template an iOS plugin.
///
222 223 224 225 226 227 228 229 230
/// The [name] of the plugin is required. Additionally, either:
/// - [defaultPackage], or
/// - an implementation consisting of:
///   - the [pluginClass] (with optional [classPrefix]) that will be the entry
///     point to the plugin's native code, and/or
///   - the [dartPluginClass] that will be the entry point for the plugin's
///     Dart code
/// is required.
class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin {
231
  const IOSPlugin({
232 233
    required this.name,
    required this.classPrefix,
234 235
    this.pluginClass,
    this.dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
236
    bool? ffiPlugin,
237
    this.defaultPackage,
Daco Harkes's avatar
Daco Harkes committed
238
  }) : ffiPlugin = ffiPlugin ?? false;
239 240

  factory IOSPlugin.fromYaml(String name, YamlMap yaml) {
241
    assert(validate(yaml)); // TODO(zanderso): https://github.com/flutter/flutter/issues/67241
242 243 244
    return IOSPlugin(
      name: name,
      classPrefix: '',
245 246
      pluginClass: yaml[kPluginClass] as String?,
      dartPluginClass: yaml[kDartPluginClass] as String?,
Daco Harkes's avatar
Daco Harkes committed
247
      ffiPlugin: yaml[kFfiPlugin] as bool?,
248
      defaultPackage: yaml[kDefaultPackage] as String?,
249 250 251 252 253 254 255
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
256
    return yaml[kPluginClass] is String ||
Daco Harkes's avatar
Daco Harkes committed
257 258 259
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kDefaultPackage] is String;
260 261 262 263 264 265 266 267 268
  }

  static const String kConfigKey = 'ios';

  final String name;

  /// Note, this is here only for legacy reasons. Multi-platform format
  /// always sets it to empty String.
  final String classPrefix;
269 270
  final String? pluginClass;
  final String? dartPluginClass;
Daco Harkes's avatar
Daco Harkes committed
271
  final bool ffiPlugin;
272 273 274
  final String? defaultPackage;

  @override
Daco Harkes's avatar
Daco Harkes committed
275 276 277 278 279 280 281
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;
282 283 284 285 286 287

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      'prefix': classPrefix,
288 289
      if (pluginClass != null) 'class': pluginClass,
      if (dartPluginClass != null) kDartPluginClass : dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
290
      if (ffiPlugin) kFfiPlugin: true,
291
      if (defaultPackage != null) kDefaultPackage : defaultPackage,
292 293 294 295 296 297
    };
  }
}

/// Contains the parameters to template a macOS plugin.
///
Daco Harkes's avatar
Daco Harkes committed
298 299
/// The [name] of the plugin is required. Either [dartPluginClass] or
/// [pluginClass] or [ffiPlugin] are required.
300 301
/// [pluginClass] will be the entry point to the plugin's native code.
class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin {
302
  const MacOSPlugin({
303
    required this.name,
304 305
    this.pluginClass,
    this.dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
306
    bool? ffiPlugin,
307
    this.defaultPackage,
Daco Harkes's avatar
Daco Harkes committed
308
  }) : ffiPlugin = ffiPlugin ?? false;
309 310 311

  factory MacOSPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
312
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
313
    String? pluginClass = yaml[kPluginClass] as String?;
314 315 316
    if (pluginClass == 'none') {
      pluginClass = null;
    }
317 318
    return MacOSPlugin(
      name: name,
319
      pluginClass: pluginClass,
320
      dartPluginClass: yaml[kDartPluginClass] as String?,
Daco Harkes's avatar
Daco Harkes committed
321
      ffiPlugin: yaml[kFfiPlugin] as bool?,
322
      defaultPackage: yaml[kDefaultPackage] as String?,
323 324 325 326 327 328 329
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
330
    return yaml[kPluginClass] is String ||
Daco Harkes's avatar
Daco Harkes committed
331 332 333
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kDefaultPackage] is String;
334 335 336 337 338
  }

  static const String kConfigKey = 'macos';

  final String name;
339 340
  final String? pluginClass;
  final String? dartPluginClass;
Daco Harkes's avatar
Daco Harkes committed
341
  final bool ffiPlugin;
342
  final String? defaultPackage;
343 344

  @override
Daco Harkes's avatar
Daco Harkes committed
345 346 347 348 349 350 351
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;
352 353 354 355 356

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
357
      if (pluginClass != null) 'class': pluginClass,
Daco Harkes's avatar
Daco Harkes committed
358 359 360
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
      if (ffiPlugin) kFfiPlugin: true,
      if (defaultPackage != null) kDefaultPackage: defaultPackage,
361 362 363
    };
  }
}
364

365 366
/// Contains the parameters to template a Windows plugin.
///
367 368
/// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required.
/// [pluginClass] will be the entry point to the plugin's native code.
Daco Harkes's avatar
Daco Harkes committed
369 370
class WindowsPlugin extends PluginPlatform
    implements NativeOrDartPlugin, VariantPlatformPlugin {
371
  const WindowsPlugin({
372
    required this.name,
373 374
    this.pluginClass,
    this.dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
375
    bool? ffiPlugin,
376
    this.defaultPackage,
377
    this.variants = const <PluginPlatformVariant>{},
Daco Harkes's avatar
Daco Harkes committed
378 379
  })  : ffiPlugin = ffiPlugin ?? false,
        assert(pluginClass != null || dartPluginClass != null || defaultPackage != null);
380 381 382

  factory WindowsPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
383
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
384
    String? pluginClass = yaml[kPluginClass] as String?;
385 386 387
    if (pluginClass == 'none') {
      pluginClass = null;
    }
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
    final Set<PluginPlatformVariant> variants = <PluginPlatformVariant>{};
    final YamlList? variantList = yaml[kSupportedVariants] as YamlList?;
    if (variantList == null) {
      // If no variant list is provided assume Win32 for backward compatibility.
      variants.add(PluginPlatformVariant.win32);
    } else {
      const Map<String, PluginPlatformVariant> variantByName = <String, PluginPlatformVariant>{
        'win32': PluginPlatformVariant.win32,
      };
      for (final String variantName in variantList.cast<String>()) {
        final PluginPlatformVariant? variant = variantByName[variantName];
        if (variant != null) {
          variants.add(variant);
        }
        // Ignore unrecognized variants to make adding new variants in the
        // future non-breaking.
      }
    }
406 407
    return WindowsPlugin(
      name: name,
408
      pluginClass: pluginClass,
409
      dartPluginClass: yaml[kDartPluginClass] as String?,
Daco Harkes's avatar
Daco Harkes committed
410
      ffiPlugin: yaml[kFfiPlugin] as bool?,
411
      defaultPackage: yaml[kDefaultPackage] as String?,
412
      variants: variants,
413 414 415 416 417 418 419
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
420

421
    return yaml[kPluginClass] is String ||
Daco Harkes's avatar
Daco Harkes committed
422 423 424
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kDefaultPackage] is String;
425 426 427 428 429
  }

  static const String kConfigKey = 'windows';

  final String name;
430 431
  final String? pluginClass;
  final String? dartPluginClass;
Daco Harkes's avatar
Daco Harkes committed
432
  final bool ffiPlugin;
433
  final String? defaultPackage;
434 435 436 437
  final Set<PluginPlatformVariant> variants;

  @override
  Set<PluginPlatformVariant> get supportedVariants => variants;
438 439

  @override
Daco Harkes's avatar
Daco Harkes committed
440 441 442 443 444 445 446
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;
447 448 449 450 451

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
452
      if (pluginClass != null) 'class': pluginClass,
453
      if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!),
454
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
455
      if (ffiPlugin) kFfiPlugin: true,
456
      if (defaultPackage != null) kDefaultPackage: defaultPackage,
457 458 459 460 461 462
    };
  }
}

/// Contains the parameters to template a Linux plugin.
///
463 464 465
/// The [name] of the plugin is required. Either [dartPluginClass] or [pluginClass] are required.
/// [pluginClass] will be the entry point to the plugin's native code.
class LinuxPlugin extends PluginPlatform implements NativeOrDartPlugin {
466
  const LinuxPlugin({
467
    required this.name,
468 469
    this.pluginClass,
    this.dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
470
    bool? ffiPlugin,
471
    this.defaultPackage,
Daco Harkes's avatar
Daco Harkes committed
472
  })  : ffiPlugin = ffiPlugin ?? false,
473
        assert(pluginClass != null || dartPluginClass != null || (ffiPlugin ?? false) || defaultPackage != null);
474 475 476

  factory LinuxPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
477
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
478
    String? pluginClass = yaml[kPluginClass] as String?;
479 480 481
    if (pluginClass == 'none') {
      pluginClass = null;
    }
482 483
    return LinuxPlugin(
      name: name,
484
      pluginClass: pluginClass,
485
      dartPluginClass: yaml[kDartPluginClass] as String?,
Daco Harkes's avatar
Daco Harkes committed
486
      ffiPlugin: yaml[kFfiPlugin] as bool?,
487
      defaultPackage: yaml[kDefaultPackage] as String?,
488 489 490 491 492 493 494
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
495
    return yaml[kPluginClass] is String ||
Daco Harkes's avatar
Daco Harkes committed
496 497 498
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kDefaultPackage] is String;
499 500 501 502 503
  }

  static const String kConfigKey = 'linux';

  final String name;
504 505
  final String? pluginClass;
  final String? dartPluginClass;
Daco Harkes's avatar
Daco Harkes committed
506
  final bool ffiPlugin;
507
  final String? defaultPackage;
508 509

  @override
Daco Harkes's avatar
Daco Harkes committed
510 511 512 513 514 515 516
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;
517 518 519 520 521

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
522
      if (pluginClass != null) 'class': pluginClass,
523
      if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!),
524
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
525
      if (ffiPlugin) kFfiPlugin: true,
526
      if (defaultPackage != null) kDefaultPackage: defaultPackage,
527 528 529 530
    };
  }
}

531 532 533 534 535 536 537
/// Contains the parameters to template a web plugin.
///
/// The required fields include: [name] of the plugin, the [pluginClass] that will
/// be the entry point to the plugin's implementation, and the [fileName]
/// containing the code.
class WebPlugin extends PluginPlatform {
  const WebPlugin({
538 539 540
    required this.name,
    required this.pluginClass,
    required this.fileName,
541 542 543 544 545 546
  });

  factory WebPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
    return WebPlugin(
      name: name,
547 548
      pluginClass: yaml['pluginClass'] as String,
      fileName: yaml['fileName'] as String,
549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
    return yaml['pluginClass'] is String && yaml['fileName'] is String;
  }

  static const String kConfigKey = 'web';

  /// The name of the plugin.
  final String name;

  /// The class containing the plugin implementation details.
  ///
  /// This class should have a static `registerWith` method defined.
  final String pluginClass;

  /// The name of the file containing the class implementation above.
  final String fileName;

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      'class': pluginClass,
      'file': fileName,
    };
  }
}
581 582 583 584 585 586 587 588

final RegExp _internalCapitalLetterRegex = RegExp(r'(?=(?!^)[A-Z])');
String _filenameForCppClass(String className) {
  return className.splitMapJoin(
    _internalCapitalLetterRegex,
    onMatch: (_) => '_',
    onNonMatch: (String n) => n.toLowerCase());
}