platform_plugins.dart 14.1 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 13 14 15
/// Constant for 'pluginClass' key in plugin maps.
const String kPluginClass = 'pluginClass';

/// Constant for 'pluginClass' key in plugin maps.
const String kDartPluginClass = 'dartPluginClass';

16 17 18
// Constant for 'defaultPackage' key in plugin maps.
const String kDefaultPackage = 'default_package';

19 20 21 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,

  // UWP variant of Windows.
  winuwp,
}

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 45 46 47 48 49
abstract class NativeOrDartPlugin {
  /// Determines whether the plugin has a native implementation or if it's a
  /// Dart-only plugin.
  bool isNative();
}

50 51 52 53 54
/// Contains parameters to template an Android plugin.
///
/// The required fields include: [name] of the plugin, [package] of the plugin and
/// the [pluginClass] that will be the entry point to the plugin's native code.
class AndroidPlugin extends PluginPlatform {
55
  AndroidPlugin({
56 57 58 59 60
    required this.name,
    required this.package,
    required this.pluginClass,
    required this.pluginPath,
    required FileSystem fileSystem,
61
  }) : _fileSystem = fileSystem;
62

63
  factory AndroidPlugin.fromYaml(String name, YamlMap yaml, String pluginPath, FileSystem fileSystem) {
64 65 66
    assert(validate(yaml));
    return AndroidPlugin(
      name: name,
67 68
      package: yaml['package'] as String,
      pluginClass: yaml['pluginClass'] as String,
69
      pluginPath: pluginPath,
70
      fileSystem: fileSystem,
71 72 73
    );
  }

74 75
  final FileSystem _fileSystem;

76 77 78 79 80 81 82 83 84
  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
    return yaml['package'] is String && yaml['pluginClass'] is String;
  }

  static const String kConfigKey = 'android';

85
  /// The plugin name defined in pubspec.yaml.
86
  final String name;
87 88

  /// The plugin package name defined in pubspec.yaml.
89
  final String package;
90 91

  /// The plugin main class defined in pubspec.yaml.
92 93
  final String pluginClass;

94 95 96
  /// The absolute path to the plugin in the pub cache.
  final String pluginPath;

97 98 99 100 101 102
  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      'package': package,
      'class': pluginClass,
103
      // Mustache doesn't support complex types.
104 105
      'supportsEmbeddingV1': _supportedEmbeddings.contains('1'),
      'supportsEmbeddingV2': _supportedEmbeddings.contains('2'),
106 107
    };
  }
108

109
  Set<String>? _cachedEmbeddingVersion;
110 111

  /// Returns the version of the Android embedding.
112
  Set<String> get _supportedEmbeddings => _cachedEmbeddingVersion ??= _getSupportedEmbeddings();
113

114
  Set<String> _getSupportedEmbeddings() {
115
    assert(pluginPath != null);
116
    final Set<String> supportedEmbeddings = <String>{};
117
    final String baseMainPath = _fileSystem.path.join(
118 119 120 121 122
      pluginPath,
      'android',
      'src',
      'main',
    );
123 124

    final List<String> mainClassCandidates = <String>[
125
      _fileSystem.path.join(
126 127
        baseMainPath,
        'java',
128
        package.replaceAll('.', _fileSystem.path.separator),
129
        '$pluginClass.java',
130
      ),
131
      _fileSystem.path.join(
132 133
        baseMainPath,
        'kotlin',
134
        package.replaceAll('.', _fileSystem.path.separator),
135
        '$pluginClass.kt',
136
      )
137 138
    ];

139
    File? mainPluginClass;
140 141
    bool mainClassFound = false;
    for (final String mainClassCandidate in mainClassCandidates) {
142
      mainPluginClass = _fileSystem.file(mainClassCandidate);
143 144 145 146 147
      if (mainPluginClass.existsSync()) {
        mainClassFound = true;
        break;
      }
    }
148
    if (mainPluginClass == null || !mainClassFound) {
149 150
      assert(mainClassCandidates.length <= 2);
      throwToolExit(
151 152
        "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"
153 154
        '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. '
155 156
      );
    }
157

158
    final String mainClassContent = mainPluginClass.readAsStringSync();
159 160
    if (mainClassContent
        .contains('io.flutter.embedding.engine.plugins.FlutterPlugin')) {
161 162 163
      supportedEmbeddings.add('2');
    } else {
      supportedEmbeddings.add('1');
164
    }
165 166 167 168 169
    if (mainClassContent.contains('PluginRegistry')
        && mainClassContent.contains('registerWith')) {
      supportedEmbeddings.add('1');
    }
    return supportedEmbeddings;
170
  }
171 172 173 174 175 176 177 178
}

/// Contains the parameters to template an iOS plugin.
///
/// The required fields include: [name] of the plugin, the [pluginClass] that
/// will be the entry point to the plugin's native code.
class IOSPlugin extends PluginPlatform {
  const IOSPlugin({
179 180 181
    required this.name,
    required this.classPrefix,
    required this.pluginClass,
182 183 184
  });

  factory IOSPlugin.fromYaml(String name, YamlMap yaml) {
185
    assert(validate(yaml)); // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/67241
186 187 188
    return IOSPlugin(
      name: name,
      classPrefix: '',
189
      pluginClass: yaml['pluginClass'] as String,
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
    );
  }

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

  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;
  final String pluginClass;

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
      'prefix': classPrefix,
      'class': pluginClass,
    };
  }
}

/// Contains the parameters to template a macOS plugin.
///
221 222 223
/// 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 MacOSPlugin extends PluginPlatform implements NativeOrDartPlugin {
224
  const MacOSPlugin({
225
    required this.name,
226 227
    this.pluginClass,
    this.dartPluginClass,
228
    this.defaultPackage,
229 230 231 232
  });

  factory MacOSPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
233
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
234
    String? pluginClass = yaml[kPluginClass] as String?;
235 236 237
    if (pluginClass == 'none') {
      pluginClass = null;
    }
238 239
    return MacOSPlugin(
      name: name,
240
      pluginClass: pluginClass,
241
      dartPluginClass: yaml[kDartPluginClass] as String?,
242
      defaultPackage: yaml[kDefaultPackage] as String?,
243 244 245 246 247 248 249
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
250 251 252
    return yaml[kPluginClass] is String ||
           yaml[kDartPluginClass] is String ||
           yaml[kDefaultPackage] is String;
253 254 255 256 257
  }

  static const String kConfigKey = 'macos';

  final String name;
258 259
  final String? pluginClass;
  final String? dartPluginClass;
260
  final String? defaultPackage;
261 262 263

  @override
  bool isNative() => pluginClass != null;
264 265 266 267 268

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
269
      if (pluginClass != null) 'class': pluginClass,
270 271
      if (dartPluginClass != null) kDartPluginClass : dartPluginClass,
      if (defaultPackage != null) kDefaultPackage : defaultPackage,
272 273 274
    };
  }
}
275

276 277
/// Contains the parameters to template a Windows plugin.
///
278 279
/// 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.
280
class WindowsPlugin extends PluginPlatform implements NativeOrDartPlugin, VariantPlatformPlugin {
281
  const WindowsPlugin({
282
    required this.name,
283 284
    this.pluginClass,
    this.dartPluginClass,
285
    this.defaultPackage,
286
    this.variants = const <PluginPlatformVariant>{},
287
  }) : assert(pluginClass != null || dartPluginClass != null || defaultPackage != null);
288 289 290

  factory WindowsPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
291
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
292
    String? pluginClass = yaml[kPluginClass] as String?;
293 294 295
    if (pluginClass == 'none') {
      pluginClass = null;
    }
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
    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,
        'uwp': PluginPlatformVariant.winuwp,
      };
      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.
      }
    }
315 316
    return WindowsPlugin(
      name: name,
317
      pluginClass: pluginClass,
318
      dartPluginClass: yaml[kDartPluginClass] as String?,
319
      defaultPackage: yaml[kDefaultPackage] as String?,
320
      variants: variants,
321 322 323 324 325 326 327
    );
  }

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

329 330 331
    return yaml[kPluginClass] is String ||
           yaml[kDartPluginClass] is String ||
           yaml[kDefaultPackage] is String;
332 333 334 335 336
  }

  static const String kConfigKey = 'windows';

  final String name;
337 338
  final String? pluginClass;
  final String? dartPluginClass;
339
  final String? defaultPackage;
340 341 342 343
  final Set<PluginPlatformVariant> variants;

  @override
  Set<PluginPlatformVariant> get supportedVariants => variants;
344 345 346

  @override
  bool isNative() => pluginClass != null;
347 348 349 350 351

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
352
      if (pluginClass != null) 'class': pluginClass!,
353
      if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!),
354 355
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass!,
      if (defaultPackage != null) kDefaultPackage: defaultPackage!,
356 357 358 359 360 361
    };
  }
}

/// Contains the parameters to template a Linux plugin.
///
362 363 364
/// 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 {
365
  const LinuxPlugin({
366
    required this.name,
367 368
    this.pluginClass,
    this.dartPluginClass,
369 370
    this.defaultPackage,
  }) : assert(pluginClass != null || dartPluginClass != null || defaultPackage != null);
371 372 373

  factory LinuxPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
374
    // Treat 'none' as not present. See https://github.com/flutter/flutter/issues/57497.
375
    String? pluginClass = yaml[kPluginClass] as String?;
376 377 378
    if (pluginClass == 'none') {
      pluginClass = null;
    }
379 380
    return LinuxPlugin(
      name: name,
381
      pluginClass: pluginClass,
382
      dartPluginClass: yaml[kDartPluginClass] as String?,
383
      defaultPackage: yaml[kDefaultPackage] as String?,
384 385 386 387 388 389 390
    );
  }

  static bool validate(YamlMap yaml) {
    if (yaml == null) {
      return false;
    }
391 392 393
    return yaml[kPluginClass] is String ||
           yaml[kDartPluginClass] is String ||
           yaml[kDefaultPackage] is String;
394 395 396 397 398
  }

  static const String kConfigKey = 'linux';

  final String name;
399 400
  final String? pluginClass;
  final String? dartPluginClass;
401
  final String? defaultPackage;
402 403 404

  @override
  bool isNative() => pluginClass != null;
405 406 407 408 409

  @override
  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'name': name,
410
      if (pluginClass != null) 'class': pluginClass!,
411
      if (pluginClass != null) 'filename': _filenameForCppClass(pluginClass!),
412 413
      if (dartPluginClass != null) kDartPluginClass: dartPluginClass!,
      if (defaultPackage != null) kDefaultPackage: defaultPackage!,
414 415 416 417
    };
  }
}

418 419 420 421 422 423 424
/// 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({
425 426 427
    required this.name,
    required this.pluginClass,
    required this.fileName,
428 429 430 431 432 433
  });

  factory WebPlugin.fromYaml(String name, YamlMap yaml) {
    assert(validate(yaml));
    return WebPlugin(
      name: name,
434 435
      pluginClass: yaml['pluginClass'] as String,
      fileName: yaml['fileName'] as String,
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467
    );
  }

  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,
    };
  }
}
468 469 470 471 472 473 474 475

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