platform_plugins.dart 18.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
/// Constant for 'sharedDarwinSource' key in plugin maps.
/// Can be set for iOS and macOS plugins.
const String kSharedDarwinSource = 'sharedDarwinSource';

26 27 28 29 30 31 32 33 34
/// 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,
}

35
/// Marker interface for all platform specific plugin config implementations.
36 37 38 39 40 41
abstract class PluginPlatform {
  const PluginPlatform();

  Map<String, dynamic> toMap();
}

42 43 44 45 46 47
/// A plugin that has platform variants.
abstract class VariantPlatformPlugin {
  /// The platform variants supported by the plugin.
  Set<PluginPlatformVariant> get supportedVariants;
}

48
abstract class NativeOrDartPlugin {
Daco Harkes's avatar
Daco Harkes committed
49 50 51 52 53 54 55 56
  /// 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();
57 58
}

59 60 61 62 63
abstract class DarwinPlugin {
  /// Indicates the iOS and macOS native code is shareable the subdirectory "darwin",
  bool get sharedDarwinSource;
}

64 65
/// Contains parameters to template an Android plugin.
///
66 67 68 69 70 71 72 73 74
/// 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 {
75
  AndroidPlugin({
76 77
    required this.name,
    required this.pluginPath,
78 79 80
    this.package,
    this.pluginClass,
    this.dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
81
    bool? ffiPlugin,
82
    this.defaultPackage,
83
    required FileSystem fileSystem,
Daco Harkes's avatar
Daco Harkes committed
84 85
  })  : _fileSystem = fileSystem,
        ffiPlugin = ffiPlugin ?? false;
86

87
  factory AndroidPlugin.fromYaml(String name, YamlMap yaml, String pluginPath, FileSystem fileSystem) {
88 89 90
    assert(validate(yaml));
    return AndroidPlugin(
      name: name,
91 92 93
      package: yaml['package'] as String?,
      pluginClass: yaml[kPluginClass] as String?,
      dartPluginClass: yaml[kDartPluginClass] as String?,
Daco Harkes's avatar
Daco Harkes committed
94
      ffiPlugin: yaml[kFfiPlugin] as bool?,
95
      defaultPackage: yaml[kDefaultPackage] as String?,
96
      pluginPath: pluginPath,
97
      fileSystem: fileSystem,
98 99 100
    );
  }

101 102
  final FileSystem _fileSystem;

103
  @override
Daco Harkes's avatar
Daco Harkes committed
104 105 106 107 108 109 110
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

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

112
  static bool validate(YamlMap yaml) {
Daco Harkes's avatar
Daco Harkes committed
113 114 115 116
    return (yaml['package'] is String && yaml[kPluginClass] is String) ||
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kDefaultPackage] is String;
117 118 119 120
  }

  static const String kConfigKey = 'android';

121
  /// The plugin name defined in pubspec.yaml.
122
  final String name;
123 124

  /// The plugin package name defined in pubspec.yaml.
125 126 127 128 129 130 131
  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;
132

Daco Harkes's avatar
Daco Harkes committed
133 134 135
  /// Is FFI plugin defined in pubspec.yaml.
  final bool ffiPlugin;

136 137
  /// The default implementation package defined in pubspec.yaml, if any.
  final String? defaultPackage;
138

139 140 141
  /// The absolute path to the plugin in the pub cache.
  final String pluginPath;

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

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

160 161
  Set<String> _getSupportedEmbeddings() {
    final Set<String> supportedEmbeddings = <String>{};
162
    final String baseMainPath = _fileSystem.path.join(
163 164 165 166 167
      pluginPath,
      'android',
      'src',
      'main',
    );
168

169 170 171 172 173 174 175
    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;
    }

176
    final List<String> mainClassCandidates = <String>[
177
      _fileSystem.path.join(
178 179
        baseMainPath,
        'java',
180
        package.replaceAll('.', _fileSystem.path.separator),
181
        '$pluginClass.java',
182
      ),
183
      _fileSystem.path.join(
184 185
        baseMainPath,
        'kotlin',
186
        package.replaceAll('.', _fileSystem.path.separator),
187
        '$pluginClass.kt',
188
      ),
189 190
    ];

191
    File? mainPluginClass;
192 193
    bool mainClassFound = false;
    for (final String mainClassCandidate in mainClassCandidates) {
194
      mainPluginClass = _fileSystem.file(mainClassCandidate);
195 196 197 198 199
      if (mainPluginClass.existsSync()) {
        mainClassFound = true;
        break;
      }
    }
200
    if (mainPluginClass == null || !mainClassFound) {
201 202
      assert(mainClassCandidates.length <= 2);
      throwToolExit(
203 204
        "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"
205 206
        '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. '
207 208
      );
    }
209

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

/// Contains the parameters to template an iOS plugin.
///
227 228 229 230 231 232 233 234
/// 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.
235
class IOSPlugin extends PluginPlatform implements NativeOrDartPlugin, DarwinPlugin {
236
  const IOSPlugin({
237 238
    required this.name,
    required this.classPrefix,
239 240
    this.pluginClass,
    this.dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
241
    bool? ffiPlugin,
242
    this.defaultPackage,
243 244 245
    bool? sharedDarwinSource,
  }) : ffiPlugin = ffiPlugin ?? false,
       sharedDarwinSource = sharedDarwinSource ?? false;
246 247

  factory IOSPlugin.fromYaml(String name, YamlMap yaml) {
248
    assert(validate(yaml)); // TODO(zanderso): https://github.com/flutter/flutter/issues/67241
249 250 251
    return IOSPlugin(
      name: name,
      classPrefix: '',
252 253
      pluginClass: yaml[kPluginClass] as String?,
      dartPluginClass: yaml[kDartPluginClass] as String?,
Daco Harkes's avatar
Daco Harkes committed
254
      ffiPlugin: yaml[kFfiPlugin] as bool?,
255
      defaultPackage: yaml[kDefaultPackage] as String?,
256
      sharedDarwinSource: yaml[kSharedDarwinSource] as bool?,
257 258 259 260
    );
  }

  static bool validate(YamlMap yaml) {
261
    return yaml[kPluginClass] is String ||
Daco Harkes's avatar
Daco Harkes committed
262 263
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
264
        yaml[kSharedDarwinSource] == true ||
Daco Harkes's avatar
Daco Harkes committed
265
        yaml[kDefaultPackage] is String;
266 267 268 269 270 271 272 273 274
  }

  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;
275 276
  final String? pluginClass;
  final String? dartPluginClass;
Daco Harkes's avatar
Daco Harkes committed
277
  final bool ffiPlugin;
278 279
  final String? defaultPackage;

280 281 282 283 284
  /// Indicates the iOS native code is shareable with macOS in
  /// the subdirectory "darwin", otherwise in the subdirectory "ios".
  @override
  final bool sharedDarwinSource;

285
  @override
Daco Harkes's avatar
Daco Harkes committed
286 287 288 289 290 291 292
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;
293 294 295 296 297 298

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      'prefix': classPrefix,
299 300
      if (pluginClass != null) 'class': pluginClass,
      if (dartPluginClass != null) kDartPluginClass : dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
301
      if (ffiPlugin) kFfiPlugin: true,
302
      if (sharedDarwinSource) kSharedDarwinSource: true,
303
      if (defaultPackage != null) kDefaultPackage : defaultPackage,
304 305 306 307 308 309
    };
  }
}

/// Contains the parameters to template a macOS plugin.
///
Daco Harkes's avatar
Daco Harkes committed
310 311
/// The [name] of the plugin is required. Either [dartPluginClass] or
/// [pluginClass] or [ffiPlugin] are required.
312
/// [pluginClass] will be the entry point to the plugin's native code.
313
class MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin, DarwinPlugin {
314
  const MacOSPlugin({
315
    required this.name,
316 317
    this.pluginClass,
    this.dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
318
    bool? ffiPlugin,
319
    this.defaultPackage,
320 321 322
    bool? sharedDarwinSource,
  }) : ffiPlugin = ffiPlugin ?? false,
       sharedDarwinSource = sharedDarwinSource ?? false;
323 324 325

  factory MacOSPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
326
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
327
    String? pluginClass = yaml[kPluginClass] as String?;
328 329 330
    if (pluginClass == 'none') {
      pluginClass = null;
    }
331 332
    return MacOSPlugin(
      name: name,
333
      pluginClass: pluginClass,
334
      dartPluginClass: yaml[kDartPluginClass] as String?,
Daco Harkes's avatar
Daco Harkes committed
335
      ffiPlugin: yaml[kFfiPlugin] as bool?,
336
      defaultPackage: yaml[kDefaultPackage] as String?,
337
      sharedDarwinSource: yaml[kSharedDarwinSource] as bool?,
338 339 340 341
    );
  }

  static bool validate(YamlMap yaml) {
342
    return yaml[kPluginClass] is String ||
Daco Harkes's avatar
Daco Harkes committed
343 344
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
345
        yaml[kSharedDarwinSource] == true ||
Daco Harkes's avatar
Daco Harkes committed
346
        yaml[kDefaultPackage] is String;
347 348 349 350 351
  }

  static const String kConfigKey = 'macos';

  final String name;
352 353
  final String? pluginClass;
  final String? dartPluginClass;
Daco Harkes's avatar
Daco Harkes committed
354
  final bool ffiPlugin;
355
  final String? defaultPackage;
356

357 358 359 360 361
  /// Indicates the macOS native code is shareable with iOS in
  /// the subdirectory "darwin", otherwise in the subdirectory "macos".
  @override
  final bool sharedDarwinSource;

362
  @override
Daco Harkes's avatar
Daco Harkes committed
363 364 365 366 367 368 369
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;
370 371 372 373 374

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
375
      if (pluginClass != null) 'class': pluginClass,
Daco Harkes's avatar
Daco Harkes committed
376 377
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
      if (ffiPlugin) kFfiPlugin: true,
378
      if (sharedDarwinSource) kSharedDarwinSource: true,
Daco Harkes's avatar
Daco Harkes committed
379
      if (defaultPackage != null) kDefaultPackage: defaultPackage,
380 381 382
    };
  }
}
383

384 385
/// Contains the parameters to template a Windows plugin.
///
386 387
/// 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
388 389
class WindowsPlugin extends PluginPlatform
    implements NativeOrDartPlugin, VariantPlatformPlugin {
390
  const WindowsPlugin({
391
    required this.name,
392 393
    this.pluginClass,
    this.dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
394
    bool? ffiPlugin,
395
    this.defaultPackage,
396
    this.variants = const <PluginPlatformVariant>{},
Daco Harkes's avatar
Daco Harkes committed
397 398
  })  : ffiPlugin = ffiPlugin ?? false,
        assert(pluginClass != null || dartPluginClass != null || defaultPackage != null);
399 400 401

  factory WindowsPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
402
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
403
    String? pluginClass = yaml[kPluginClass] as String?;
404 405 406
    if (pluginClass == 'none') {
      pluginClass = null;
    }
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
    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.
      }
    }
425 426
    return WindowsPlugin(
      name: name,
427
      pluginClass: pluginClass,
428
      dartPluginClass: yaml[kDartPluginClass] as String?,
Daco Harkes's avatar
Daco Harkes committed
429
      ffiPlugin: yaml[kFfiPlugin] as bool?,
430
      defaultPackage: yaml[kDefaultPackage] as String?,
431
      variants: variants,
432 433 434 435
    );
  }

  static bool validate(YamlMap yaml) {
436
    return yaml[kPluginClass] is String ||
Daco Harkes's avatar
Daco Harkes committed
437 438 439
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kDefaultPackage] is String;
440 441 442 443 444
  }

  static const String kConfigKey = 'windows';

  final String name;
445 446
  final String? pluginClass;
  final String? dartPluginClass;
Daco Harkes's avatar
Daco Harkes committed
447
  final bool ffiPlugin;
448
  final String? defaultPackage;
449 450 451 452
  final Set<PluginPlatformVariant> variants;

  @override
  Set<PluginPlatformVariant> get supportedVariants => variants;
453 454

  @override
Daco Harkes's avatar
Daco Harkes committed
455 456 457 458 459 460 461
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;
462 463 464 465 466

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
467
      if (pluginClass != null) 'class': pluginClass,
468
      if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!),
469
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
470
      if (ffiPlugin) kFfiPlugin: true,
471
      if (defaultPackage != null) kDefaultPackage: defaultPackage,
472 473 474 475 476 477
    };
  }
}

/// Contains the parameters to template a Linux plugin.
///
478 479 480
/// 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 {
481
  const LinuxPlugin({
482
    required this.name,
483 484
    this.pluginClass,
    this.dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
485
    bool? ffiPlugin,
486
    this.defaultPackage,
Daco Harkes's avatar
Daco Harkes committed
487
  })  : ffiPlugin = ffiPlugin ?? false,
488
        assert(pluginClass != null || dartPluginClass != null || (ffiPlugin ?? false) || defaultPackage != null);
489 490 491

  factory LinuxPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
492
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
493
    String? pluginClass = yaml[kPluginClass] as String?;
494 495 496
    if (pluginClass == 'none') {
      pluginClass = null;
    }
497 498
    return LinuxPlugin(
      name: name,
499
      pluginClass: pluginClass,
500
      dartPluginClass: yaml[kDartPluginClass] as String?,
Daco Harkes's avatar
Daco Harkes committed
501
      ffiPlugin: yaml[kFfiPlugin] as bool?,
502
      defaultPackage: yaml[kDefaultPackage] as String?,
503 504 505 506
    );
  }

  static bool validate(YamlMap yaml) {
507
    return yaml[kPluginClass] is String ||
Daco Harkes's avatar
Daco Harkes committed
508 509 510
        yaml[kDartPluginClass] is String ||
        yaml[kFfiPlugin] == true ||
        yaml[kDefaultPackage] is String;
511 512 513 514 515
  }

  static const String kConfigKey = 'linux';

  final String name;
516 517
  final String? pluginClass;
  final String? dartPluginClass;
Daco Harkes's avatar
Daco Harkes committed
518
  final bool ffiPlugin;
519
  final String? defaultPackage;
520 521

  @override
Daco Harkes's avatar
Daco Harkes committed
522 523 524 525 526 527 528
  bool hasMethodChannel() => pluginClass != null;

  @override
  bool hasFfi() => ffiPlugin;

  @override
  bool hasDart() => dartPluginClass != null;
529 530 531 532 533

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
534
      if (pluginClass != null) 'class': pluginClass,
535
      if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!),
536
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass,
Daco Harkes's avatar
Daco Harkes committed
537
      if (ffiPlugin) kFfiPlugin: true,
538
      if (defaultPackage != null) kDefaultPackage: defaultPackage,
539 540 541 542
    };
  }
}

543 544 545 546 547 548 549
/// 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({
550 551 552
    required this.name,
    required this.pluginClass,
    required this.fileName,
553 554 555
  });

  factory WebPlugin.fromYaml(String name, YamlMap yaml) {
556 557 558 559 560 561
    if (yaml['pluginClass'] is! String) {
      throwToolExit('The plugin `$name` is missing the required field `pluginClass` in pubspec.yaml');
    }
    if (yaml['fileName'] is! String) {
      throwToolExit('The plugin `$name` is missing the required field `fileName` in pubspec.yaml');
    }
562 563
    return WebPlugin(
      name: name,
564 565
      pluginClass: yaml['pluginClass'] as String,
      fileName: yaml['fileName'] as String,
566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590
    );
  }

  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,
    };
  }
}
591 592 593 594 595 596 597 598

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