plugins.dart 52.8 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 6
// @dart = 2.8

7
import 'package:meta/meta.dart';
8
import 'package:package_config/package_config.dart';
9
import 'package:path/path.dart' as path; // ignore: package_path_import
10 11
import 'package:yaml/yaml.dart';

12
import 'android/gradle.dart';
13
import 'base/common.dart';
14
import 'base/error_handling_io.dart';
15
import 'base/file_system.dart';
16 17 18
import 'base/os.dart';
import 'base/platform.dart';
import 'base/version.dart';
19
import 'convert.dart';
20
import 'dart/language_version.dart';
21
import 'dart/package_map.dart';
22
import 'features.dart';
23
import 'globals.dart' as globals;
24
import 'platform_plugins.dart';
25
import 'project.dart';
26

27
void _renderTemplateToFile(String template, dynamic context, String filePath) {
28 29
  final String renderedTemplate = globals.templateRenderer
    .renderString(template, context, htmlEscapeValues: false);
30
  final File file = globals.fs.file(filePath);
31 32 33 34
  file.createSync(recursive: true);
  file.writeAsStringSync(renderedTemplate);
}

35
class Plugin {
36
  Plugin({
37 38 39
    @required this.name,
    @required this.path,
    @required this.platforms,
40 41
    @required this.defaultPackagePlatforms,
    @required this.pluginDartClassPlatforms,
42
    @required this.dependencies,
43 44
    @required this.isDirectDependency,
    this.implementsPackage,
45 46 47
  }) : assert(name != null),
       assert(path != null),
       assert(platforms != null),
48 49 50 51
       assert(defaultPackagePlatforms != null),
       assert(pluginDartClassPlatforms != null),
       assert(dependencies != null),
       assert(isDirectDependency != null);
52

53 54 55
  /// Parses [Plugin] specification from the provided pluginYaml.
  ///
  /// This currently supports two formats. Legacy and Multi-platform.
56
  ///
57 58
  /// Example of the deprecated Legacy format.
  ///
59 60 61 62
  ///     flutter:
  ///      plugin:
  ///        androidPackage: io.flutter.plugins.sample
  ///        iosPrefix: FLT
63
  ///        pluginClass: SamplePlugin
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
  ///
  /// Example Multi-platform format.
  ///
  ///     flutter:
  ///      plugin:
  ///        platforms:
  ///          android:
  ///            package: io.flutter.plugins.sample
  ///            pluginClass: SamplePlugin
  ///          ios:
  ///            pluginClass: SamplePlugin
  ///          linux:
  ///            pluginClass: SamplePlugin
  ///          macos:
  ///            pluginClass: SamplePlugin
  ///          windows:
  ///            pluginClass: SamplePlugin
81 82 83 84
  factory Plugin.fromYaml(
    String name,
    String path,
    YamlMap pluginYaml,
85 86
    List<String> dependencies, {
    @required FileSystem fileSystem,
87
    Set<String> appDependencies,
88
  }) {
89 90
    final List<String> errors = validatePluginYaml(pluginYaml);
    if (errors.isNotEmpty) {
91
      throwToolExit('Invalid plugin specification $name.\n${errors.join('\n')}');
92 93
    }
    if (pluginYaml != null && pluginYaml['platforms'] != null) {
94 95 96 97 98 99 100 101
      return Plugin._fromMultiPlatformYaml(
        name,
        path,
        pluginYaml,
        dependencies,
        fileSystem,
        appDependencies != null && appDependencies.contains(name),
      );
102
    }
103 104 105 106 107 108 109 110
    return Plugin._fromLegacyYaml(
      name,
      path,
      pluginYaml,
      dependencies,
      fileSystem,
      appDependencies != null && appDependencies.contains(name),
    );
111 112
  }

113 114 115 116 117
  factory Plugin._fromMultiPlatformYaml(
    String name,
    String path,
    dynamic pluginYaml,
    List<String> dependencies,
118
    FileSystem fileSystem,
119
    bool isDirectDependency,
120
  ) {
121
    assert (pluginYaml != null && pluginYaml['platforms'] != null,
122
            'Invalid multi-platform plugin specification $name.');
123
    final YamlMap platformsYaml = pluginYaml['platforms'] as YamlMap;
124 125

    assert (_validateMultiPlatformYaml(platformsYaml).isEmpty,
126
            'Invalid multi-platform plugin specification $name.');
127 128 129

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

130
    if (_providesImplementationForPlatform(platformsYaml, AndroidPlugin.kConfigKey)) {
131 132
      platforms[AndroidPlugin.kConfigKey] = AndroidPlugin.fromYaml(
        name,
133
        platformsYaml[AndroidPlugin.kConfigKey] as YamlMap,
134
        path,
135
        fileSystem,
136
      );
137 138
    }

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

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

149
    if (_providesImplementationForPlatform(platformsYaml, MacOSPlugin.kConfigKey)) {
150
      platforms[MacOSPlugin.kConfigKey] =
151
          MacOSPlugin.fromYaml(name, platformsYaml[MacOSPlugin.kConfigKey] as YamlMap);
152 153
    }

154
    if (_providesImplementationForPlatform(platformsYaml, WebPlugin.kConfigKey)) {
155
      platforms[WebPlugin.kConfigKey] =
156
          WebPlugin.fromYaml(name, platformsYaml[WebPlugin.kConfigKey] as YamlMap);
157 158
    }

159
    if (_providesImplementationForPlatform(platformsYaml, WindowsPlugin.kConfigKey)) {
160
      platforms[WindowsPlugin.kConfigKey] =
161
          WindowsPlugin.fromYaml(name, platformsYaml[WindowsPlugin.kConfigKey] as YamlMap);
162 163
    }

164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
    final String defaultPackageForLinux =
        _getDefaultPackageForPlatform(platformsYaml, LinuxPlugin.kConfigKey);

    final String defaultPackageForMacOS =
        _getDefaultPackageForPlatform(platformsYaml, MacOSPlugin.kConfigKey);

    final String defaultPackageForWindows =
        _getDefaultPackageForPlatform(platformsYaml, WindowsPlugin.kConfigKey);

    final String defaultPluginDartClassForLinux =
        _getPluginDartClassForPlatform(platformsYaml, LinuxPlugin.kConfigKey);

    final String defaultPluginDartClassForMacOS =
        _getPluginDartClassForPlatform(platformsYaml, MacOSPlugin.kConfigKey);

    final String defaultPluginDartClassForWindows =
        _getPluginDartClassForPlatform(platformsYaml, WindowsPlugin.kConfigKey);

182 183 184 185
    return Plugin(
      name: name,
      path: path,
      platforms: platforms,
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
      defaultPackagePlatforms: <String, String>{
        if (defaultPackageForLinux != null)
          LinuxPlugin.kConfigKey : defaultPackageForLinux,
        if (defaultPackageForMacOS != null)
          MacOSPlugin.kConfigKey : defaultPackageForMacOS,
        if (defaultPackageForWindows != null)
          WindowsPlugin.kConfigKey : defaultPackageForWindows,
      },
      pluginDartClassPlatforms: <String, String>{
        if (defaultPluginDartClassForLinux != null)
          LinuxPlugin.kConfigKey : defaultPluginDartClassForLinux,
        if (defaultPluginDartClassForMacOS != null)
          MacOSPlugin.kConfigKey : defaultPluginDartClassForMacOS,
        if (defaultPluginDartClassForWindows != null)
          WindowsPlugin.kConfigKey : defaultPluginDartClassForWindows,
      },
202
      dependencies: dependencies,
203 204
      isDirectDependency: isDirectDependency,
      implementsPackage: pluginYaml['implements'] != null ? pluginYaml['implements'] as String : '',
205 206 207
    );
  }

208 209 210 211 212
  factory Plugin._fromLegacyYaml(
    String name,
    String path,
    dynamic pluginYaml,
    List<String> dependencies,
213
    FileSystem fileSystem,
214
    bool isDirectDependency,
215
  ) {
216
    final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
217
    final String pluginClass = pluginYaml['pluginClass'] as String;
218
    if (pluginYaml != null && pluginClass != null) {
219
      final String androidPackage = pluginYaml['androidPackage'] as String;
220
      if (androidPackage != null) {
221 222
        platforms[AndroidPlugin.kConfigKey] = AndroidPlugin(
          name: name,
223
          package: pluginYaml['androidPackage'] as String,
224 225
          pluginClass: pluginClass,
          pluginPath: path,
226
          fileSystem: fileSystem,
227
        );
228 229
      }

230
      final String iosPrefix = pluginYaml['iosPrefix'] as String ?? '';
231 232 233 234 235 236
      platforms[IOSPlugin.kConfigKey] =
          IOSPlugin(
            name: name,
            classPrefix: iosPrefix,
            pluginClass: pluginClass,
          );
237
    }
238
    return Plugin(
239 240
      name: name,
      path: path,
241
      platforms: platforms,
242 243
      defaultPackagePlatforms: <String, String>{},
      pluginDartClassPlatforms: <String, String>{},
244
      dependencies: dependencies,
245
      isDirectDependency: isDirectDependency,
246
    );
247
  }
248

249 250 251
  /// Create a YamlMap that represents the supported platforms.
  ///
  /// For example, if the `platforms` contains 'ios' and 'android', the return map looks like:
252 253 254 255 256 257
  ///
  ///     android:
  ///       package: io.flutter.plugins.sample
  ///       pluginClass: SamplePlugin
  ///     ios:
  ///       pluginClass: SamplePlugin
258 259 260 261 262 263 264 265 266 267 268
  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);
  }

269
  static List<String> validatePluginYaml(YamlMap yaml) {
270 271 272
    if (yaml == null) {
      return <String>['Invalid "plugin" specification.'];
    }
273 274 275 276 277 278 279 280 281 282 283

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

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

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

290 291 292 293 294 295 296 297
    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];
    }

298
    if (usesNewPluginFormat) {
299 300 301 302
      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];
      }
303
      return _validateMultiPlatformYaml(yaml['platforms'] as YamlMap);
304 305 306 307 308 309
    } else {
      return _validateLegacyYaml(yaml);
    }
  }

  static List<String> _validateMultiPlatformYaml(YamlMap yaml) {
310
    bool isInvalid(String key, bool Function(YamlMap) validate) {
311 312 313
      if (!yaml.containsKey(key)) {
        return false;
      }
314
      final dynamic value = yaml[key];
315
      if (value is! YamlMap) {
316
        return true;
317
      }
318 319
      final YamlMap yamlValue = value as YamlMap;
      if (yamlValue.containsKey('default_package')) {
320 321
        return false;
      }
322
      return !validate(yamlValue);
323
    }
324 325 326 327

    if (yaml == null) {
      return <String>['Invalid "platforms" specification.'];
    }
328
    final List<String> errors = <String>[];
329
    if (isInvalid(AndroidPlugin.kConfigKey, AndroidPlugin.validate)) {
330 331
      errors.add('Invalid "android" plugin specification.');
    }
332
    if (isInvalid(IOSPlugin.kConfigKey, IOSPlugin.validate)) {
333 334
      errors.add('Invalid "ios" plugin specification.');
    }
335
    if (isInvalid(LinuxPlugin.kConfigKey, LinuxPlugin.validate)) {
336 337
      errors.add('Invalid "linux" plugin specification.');
    }
338
    if (isInvalid(MacOSPlugin.kConfigKey, MacOSPlugin.validate)) {
339 340
      errors.add('Invalid "macos" plugin specification.');
    }
341
    if (isInvalid(WindowsPlugin.kConfigKey, WindowsPlugin.validate)) {
342 343
      errors.add('Invalid "windows" plugin specification.');
    }
344 345 346 347 348
    return errors;
  }

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

350 351 352 353 354 355 356 357 358 359 360 361
    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;
  }

362
  static bool _supportsPlatform(YamlMap platformsYaml, String platformKey) {
363 364 365
    if (!platformsYaml.containsKey(platformKey)) {
      return false;
    }
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
    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)) {
397 398 399 400 401
      return false;
    }
    return true;
  }

402 403
  final String name;
  final String path;
404

405 406 407 408
  /// The name of the interface package that this plugin implements.
  /// If [null], this plugin doesn't implement an interface.
  final String implementsPackage;

409 410 411
  /// The name of the packages this plugin depends on.
  final List<String> dependencies;

412 413
  /// This is a mapping from platform config key to the plugin platform spec.
  final Map<String, PluginPlatform> platforms;
414 415 416 417 418 419 420 421 422 423

  /// 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;
424 425
}

426
Plugin _pluginFromPackage(String name, Uri packageRoot, Set<String> appDependencies) {
427 428
  final String pubspecPath = globals.fs.path.fromUri(packageRoot.resolve('pubspec.yaml'));
  if (!globals.fs.isFileSync(pubspecPath)) {
429
    return null;
430
  }
431 432 433 434 435 436 437 438
  dynamic pubspec;

  try {
    pubspec = loadYaml(globals.fs.file(pubspecPath).readAsStringSync());
  } on YamlException catch (err) {
    globals.printTrace('Failed to parse plugin manifest for $name: $err');
    // Do nothing, potentially not a plugin.
  }
439
  if (pubspec == null) {
440
    return null;
441
  }
442
  final dynamic flutterConfig = pubspec['flutter'];
443
  if (flutterConfig == null || !(flutterConfig.containsKey('plugin') as bool)) {
444
    return null;
445
  }
446
  final String packageRootPath = globals.fs.path.fromUri(packageRoot);
447
  final YamlMap dependencies = pubspec['dependencies'] as YamlMap;
448
  globals.printTrace('Found plugin $name at $packageRootPath');
449 450 451
  return Plugin.fromYaml(
    name,
    packageRootPath,
452
    flutterConfig['plugin'] as YamlMap,
453
    dependencies == null ? <String>[] : <String>[...dependencies.keys.cast<String>()],
454
    fileSystem: globals.fs,
455
    appDependencies: appDependencies,
456
  );
457 458
}

459
Future<List<Plugin>> findPlugins(FlutterProject project, { bool throwOnError = true}) async {
460
  final List<Plugin> plugins = <Plugin>[];
461 462
  final String packagesFile = globals.fs.path.join(
    project.directory.path,
463
    '.packages',
464
  );
465
  final PackageConfig packageConfig = await loadPackageConfigWithLogging(
466 467
    globals.fs.file(packagesFile),
    logger: globals.logger,
468
    throwOnError: throwOnError,
469 470 471
  );
  for (final Package package in packageConfig.packages) {
    final Uri packageRoot = package.packageUriRoot.resolve('..');
472 473 474 475 476
    final Plugin plugin = _pluginFromPackage(
      package.name,
      packageRoot,
      project.manifest.dependencies,
    );
477
    if (plugin != null) {
478
      plugins.add(plugin);
479
    }
480
  }
481
  return plugins;
482 483
}

484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 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 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607
/// Metadata associated with the resolution of a platform interface of a plugin.
class PluginInterfaceResolution {
  PluginInterfaceResolution({
    @required this.plugin,
    this.platform,
  }) : assert(plugin != 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],
    };
  }
}

/// Resolves the platform implementation for Dart-only plugins.
///
///   * If there are multiple direct pub dependencies on packages that implement the
///     frontend plugin for the current platform, fail.
///   * If there is a single direct dependency on a package that implements the
///     frontend plugin for the target platform, this package is the selected implementation.
///   * If there is no direct dependency on a package that implements the frontend
///     plugin for the target platform, and the frontend plugin has a default implementation
///     for the target platform the default implementation is selected.
///   * Else fail.
///
///  For more details, https://flutter.dev/go/federated-plugins.
List<PluginInterfaceResolution> resolvePlatformImplementation(
  List<Plugin> plugins, {
  bool throwOnPluginPubspecError = true,
}) {
  final List<String> platforms = <String>[
    LinuxPlugin.kConfigKey,
    MacOSPlugin.kConfigKey,
    WindowsPlugin.kConfigKey,
  ];
  final Map<String, PluginInterfaceResolution> directDependencyResolutions
      = <String, PluginInterfaceResolution>{};
  final Map<String, String> defaultImplementations = <String, String>{};
  bool didFindError = false;

  for (final Plugin plugin in plugins) {
    for (final String platform in platforms) {
      // The plugin doesn't implement this platform.
      if (plugin.platforms[platform] == null &&
          plugin.defaultPackagePlatforms[platform] == null) {
        continue;
      }
      // The plugin doesn't implement an interface, verify that it has a default implementation.
      if (plugin.implementsPackage == null || plugin.implementsPackage.isEmpty) {
        final String defaultImplementation = plugin.defaultPackagePlatforms[platform];
        if (defaultImplementation == null) {
          globals.printError(
            'Plugin `${plugin.name}` doesn\'t implement a plugin interface, nor sets '
            'a default implementation in pubspec.yaml.\n\n'
            'To set a default implementation, use:\n'
            'flutter:\n'
            '  plugin:\n'
            '    platforms:\n'
            '      $platform:\n'
            '        $kDefaultPackage: <plugin-implementation>\n'
            '\n'
            'To implement an interface, use:\n'
            'flutter:\n'
            '  plugin:\n'
            '    implements: <plugin-interface>'
            '\n'
          );
          didFindError = true;
          continue;
        }
        defaultImplementations['$platform/${plugin.name}'] = defaultImplementation;
        continue;
      }
      if (plugin.pluginDartClassPlatforms[platform] == null ||
          plugin.pluginDartClassPlatforms[platform] == 'none') {
        continue;
      }
      final String resolutionKey = '$platform/${plugin.implementsPackage}';
      if (directDependencyResolutions.containsKey(resolutionKey)) {
        final PluginInterfaceResolution currResolution = directDependencyResolutions[resolutionKey];
        if (currResolution.plugin.isDirectDependency && plugin.isDirectDependency) {
          globals.printError(
            'Plugin `${plugin.name}` implements an interface for `$platform`, which was already '
            'implemented by plugin `${currResolution.plugin.name}`.\n'
            'To fix this issue, remove either dependency from pubspec.yaml.'
            '\n\n'
          );
          didFindError = true;
        }
        if (currResolution.plugin.isDirectDependency) {
          // Use the plugin implementation added by the user as a direct dependency.
          continue;
        }
      }
      directDependencyResolutions[resolutionKey] = PluginInterfaceResolution(
        plugin: plugin,
        platform: platform,
      );
    }
  }
  if (didFindError && throwOnPluginPubspecError) {
    throwToolExit('Please resolve the errors');
  }
  final List<PluginInterfaceResolution> finalResolution = <PluginInterfaceResolution>[];
  for (final MapEntry<String, PluginInterfaceResolution> resolution in directDependencyResolutions.entries) {
    if (resolution.value.plugin.isDirectDependency) {
      finalResolution.add(resolution.value);
    } else if (defaultImplementations.containsKey(resolution.key)) {
      // Pick the default implementation.
      if (defaultImplementations[resolution.key] == resolution.value.plugin.name) {
        finalResolution.add(resolution.value);
      }
    }
  }
  return finalResolution;
}

608 609 610 611 612 613
// Key strings for the .flutter-plugins-dependencies file.
const String _kFlutterPluginsPluginListKey = 'plugins';
const String _kFlutterPluginsNameKey = 'name';
const String _kFlutterPluginsPathKey = 'path';
const String _kFlutterPluginsDependenciesKey = 'dependencies';

614 615 616 617 618 619 620 621 622 623
  /// Filters [plugins] to those supported by [platformKey].
  List<Map<String, dynamic>> _filterPluginsByPlatform(List<Plugin>plugins, String platformKey) {
    final Iterable<Plugin> platformPlugins = plugins.where((Plugin p) {
      return p.platforms.containsKey(platformKey);
    });

    final Set<String> pluginNames = platformPlugins.map((Plugin plugin) => plugin.name).toSet();
    final List<Map<String, dynamic>> list = <Map<String, dynamic>>[];
    for (final Plugin plugin in platformPlugins) {
      list.add(<String, dynamic>{
624 625 626
        _kFlutterPluginsNameKey: plugin.name,
        _kFlutterPluginsPathKey: globals.fsUtils.escapePath(plugin.path),
        _kFlutterPluginsDependenciesKey: <String>[...plugin.dependencies.where(pluginNames.contains)],
627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675
      });
    }
    return list;
  }

/// Writes the .flutter-plugins-dependencies file based on the list of plugins.
/// If there aren't any plugins, then the files aren't written to disk. The resulting
/// file looks something like this (order of keys is not guaranteed):
/// {
///   "info": "This is a generated file; do not edit or check into version control.",
///   "plugins": {
///     "ios": [
///       {
///         "name": "test",
///         "path": "test_path",
///         "dependencies": [
///           "plugin-a",
///           "plugin-b"
///         ]
///       }
///     ],
///     "android": [],
///     "macos": [],
///     "linux": [],
///     "windows": [],
///     "web": []
///   },
///   "dependencyGraph": [
///     {
///       "name": "plugin-a",
///       "dependencies": [
///         "plugin-b",
///         "plugin-c"
///       ]
///     },
///     {
///       "name": "plugin-b",
///       "dependencies": [
///         "plugin-c"
///       ]
///     },
///     {
///       "name": "plugin-c",
///       "dependencies": []
///     }
///   ],
///   "date_created": "1970-01-01 00:00:00.000",
///   "version": "0.0.0-unknown"
/// }
676
///
677 678
///
/// Finally, returns [true] if the plugins list has changed, otherwise returns [false].
679
bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) {
680 681
  final File pluginsFile = project.flutterPluginsDependenciesFile;
  if (plugins.isEmpty) {
682
    return ErrorHandlingFileSystem.deleteIfExists(pluginsFile);
683
  }
684

685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702
  final String iosKey = project.ios.pluginConfigKey;
  final String androidKey = project.android.pluginConfigKey;
  final String macosKey = project.macos.pluginConfigKey;
  final String linuxKey = project.linux.pluginConfigKey;
  final String windowsKey = project.windows.pluginConfigKey;
  final String webKey = project.web.pluginConfigKey;

  final Map<String, dynamic> pluginsMap = <String, dynamic>{};
  pluginsMap[iosKey] = _filterPluginsByPlatform(plugins, iosKey);
  pluginsMap[androidKey] = _filterPluginsByPlatform(plugins, androidKey);
  pluginsMap[macosKey] = _filterPluginsByPlatform(plugins, macosKey);
  pluginsMap[linuxKey] = _filterPluginsByPlatform(plugins, linuxKey);
  pluginsMap[windowsKey] = _filterPluginsByPlatform(plugins, windowsKey);
  pluginsMap[webKey] = _filterPluginsByPlatform(plugins, webKey);

  final Map<String, dynamic> result = <String, dynamic> {};

  result['info'] =  'This is a generated file; do not edit or check into version control.';
703
  result[_kFlutterPluginsPluginListKey] = pluginsMap;
704 705 706 707
  /// The dependencyGraph object is kept for backwards compatibility, but
  /// should be removed once migration is complete.
  /// https://github.com/flutter/flutter/issues/48918
  result['dependencyGraph'] = _createPluginLegacyDependencyGraph(plugins);
708
  result['date_created'] = globals.systemClock.now().toString();
709
  result['version'] = globals.flutterVersion.frameworkVersion;
710 711 712 713 714 715 716

  // Only notify if the plugins list has changed. [date_created] will always be different,
  // [version] is not relevant for this check.
  final String oldPluginsFileStringContent = _readFileContent(pluginsFile);
  bool pluginsChanged = true;
  if (oldPluginsFileStringContent != null) {
    pluginsChanged = oldPluginsFileStringContent.contains(pluginsMap.toString());
717
  }
718 719 720 721 722 723 724 725 726 727
  final String pluginFileContent = json.encode(result);
  pluginsFile.writeAsStringSync(pluginFileContent, flush: true);

  return pluginsChanged;
}

List<dynamic> _createPluginLegacyDependencyGraph(List<Plugin> plugins) {
  final List<dynamic> directAppDependencies = <dynamic>[];

  final Set<String> pluginNames = plugins.map((Plugin plugin) => plugin.name).toSet();
728
  for (final Plugin plugin in plugins) {
729 730 731 732 733 734
    directAppDependencies.add(<String, dynamic>{
      'name': plugin.name,
      // Extract the plugin dependencies which happen to be plugins.
      'dependencies': <String>[...plugin.dependencies.where(pluginNames.contains)],
    });
  }
735 736 737 738 739 740 741 742 743 744 745 746
  return directAppDependencies;
}

// The .flutter-plugins file will be DEPRECATED in favor of .flutter-plugins-dependencies.
// TODO(franciscojma): Remove this method once deprecated.
// https://github.com/flutter/flutter/issues/48918
//
/// Writes the .flutter-plugins files based on the list of plugins.
/// If there aren't any plugins, then the files aren't written to disk.
///
/// Finally, returns [true] if .flutter-plugins has changed, otherwise returns [false].
bool _writeFlutterPluginsListLegacy(FlutterProject project, List<Plugin> plugins) {
747
  final File pluginsFile = project.flutterPluginsFile;
748
  if (plugins.isEmpty) {
749
    return ErrorHandlingFileSystem.deleteIfExists(pluginsFile);
750
  }
751

752 753 754 755
  const String info = 'This is a generated file; do not edit or check into version control.';
  final StringBuffer flutterPluginsBuffer = StringBuffer('# $info\n');

  for (final Plugin plugin in plugins) {
756
    flutterPluginsBuffer.write('${plugin.name}=${globals.fsUtils.escapePath(plugin.path)}\n');
757
  }
758 759 760
  final String oldPluginFileContent = _readFileContent(pluginsFile);
  final String pluginFileContent = flutterPluginsBuffer.toString();
  pluginsFile.writeAsStringSync(pluginFileContent, flush: true);
761

762
  return oldPluginFileContent != _readFileContent(pluginsFile);
763 764
}

765 766 767
/// Returns the contents of [File] or [null] if that file does not exist.
String _readFileContent(File file) {
  return file.existsSync() ? file.readAsStringSync() : null;
768
}
769

770 771
const String _androidPluginRegistryTemplateOldEmbedding = '''
package io.flutter.plugins;
772

773
import io.flutter.plugin.common.PluginRegistry;
774
{{#plugins}}
775 776 777 778 779 780
import {{package}}.{{class}};
{{/plugins}}

/**
 * Generated file. Do not edit.
 */
781 782
public final class GeneratedPluginRegistrant {
  public static void registerWith(PluginRegistry registry) {
783 784 785
    if (alreadyRegisteredWith(registry)) {
      return;
    }
786
{{#plugins}}
787
    {{class}}.registerWith(registry.registrarFor("{{package}}.{{class}}"));
788
{{/plugins}}
789
  }
790 791 792 793 794 795 796 797 798

  private static boolean alreadyRegisteredWith(PluginRegistry registry) {
    final String key = GeneratedPluginRegistrant.class.getCanonicalName();
    if (registry.hasPlugin(key)) {
      return true;
    }
    registry.registrarFor(key);
    return false;
  }
799 800 801
}
''';

802 803
const String _androidPluginRegistryTemplateNewEmbedding = '''
package io.flutter.plugins;
804

805
import androidx.annotation.Keep;
806
import androidx.annotation.NonNull;
807

808 809 810 811 812 813 814 815 816 817
import io.flutter.embedding.engine.FlutterEngine;
{{#needsShim}}
import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry;
{{/needsShim}}

/**
 * Generated file. Do not edit.
 * This file is generated by the Flutter tool based on the
 * plugins that support the Android platform.
 */
818
@Keep
819 820 821 822 823 824
public final class GeneratedPluginRegistrant {
  public static void registerWith(@NonNull FlutterEngine flutterEngine) {
{{#needsShim}}
    ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine);
{{/needsShim}}
{{#plugins}}
825
  {{#supportsEmbeddingV2}}
826
    flutterEngine.getPlugins().add(new {{package}}.{{class}}());
827 828 829 830 831 832
  {{/supportsEmbeddingV2}}
  {{^supportsEmbeddingV2}}
    {{#supportsEmbeddingV1}}
      {{package}}.{{class}}.registerWith(shimPluginRegistry.registrarFor("{{package}}.{{class}}"));
    {{/supportsEmbeddingV1}}
  {{/supportsEmbeddingV2}}
833 834 835 836 837
{{/plugins}}
  }
}
''';

838 839
List<Map<String, dynamic>> _extractPlatformMaps(List<Plugin> plugins, String type) {
  final List<Map<String, dynamic>> pluginConfigs = <Map<String, dynamic>>[];
840
  for (final Plugin p in plugins) {
841 842 843 844 845 846 847 848
    final PluginPlatform platformPlugin = p.platforms[type];
    if (platformPlugin != null) {
      pluginConfigs.add(platformPlugin.toMap());
    }
  }
  return pluginConfigs;
}

849 850
/// Returns the version of the Android embedding that the current
/// [project] is using.
851
AndroidEmbeddingVersion _getAndroidEmbeddingVersion(FlutterProject project) {
852
  assert(project.android != null);
853

854
  return project.android.getEmbeddingVersion();
855 856
}

857
Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
858 859 860 861
  final List<Map<String, dynamic>> androidPlugins =
    _extractPlatformMaps(plugins, AndroidPlugin.kConfigKey);

  final Map<String, dynamic> templateContext = <String, dynamic>{
862
    'plugins': androidPlugins,
863
    'androidX': isAppUsingAndroidX(project.android.hostAppGradleRoot),
864
  };
865
  final String javaSourcePath = globals.fs.path.join(
866
    project.android.pluginRegistrantHost.path,
867 868 869 870
    'src',
    'main',
    'java',
  );
871
  final String registryPath = globals.fs.path.join(
872 873 874 875 876 877
    javaSourcePath,
    'io',
    'flutter',
    'plugins',
    'GeneratedPluginRegistrant.java',
  );
878
  String templateContent;
879
  final AndroidEmbeddingVersion appEmbeddingVersion = _getAndroidEmbeddingVersion(project);
880
  switch (appEmbeddingVersion) {
881
    case AndroidEmbeddingVersion.v2:
882 883
      templateContext['needsShim'] = false;
      // If a plugin is using an embedding version older than 2.0 and the app is using 2.0,
884
      // then add shim for the old plugins.
885
      for (final Map<String, dynamic> plugin in androidPlugins) {
886
        if (plugin['supportsEmbeddingV1'] as bool && !(plugin['supportsEmbeddingV2'] as bool)) {
887
          templateContext['needsShim'] = true;
888
          if (project.isModule) {
889
            globals.printStatus(
890 891 892 893 894 895 896 897
              'The plugin `${plugin['name']}` is built using an older version '
              "of the Android plugin API which assumes that it's running in a "
              'full-Flutter environment. It may have undefined behaviors when '
              'Flutter is integrated into an existing app as a module.\n'
              'The plugin can be updated to the v2 Android Plugin APIs by '
              'following https://flutter.dev/go/android-plugin-migration.'
            );
          }
898 899 900
        }
      }
      templateContent = _androidPluginRegistryTemplateNewEmbedding;
901 902
      break;
    case AndroidEmbeddingVersion.v1:
903
    default:
904
      for (final Map<String, dynamic> plugin in androidPlugins) {
905
        if (!(plugin['supportsEmbeddingV1'] as bool) && plugin['supportsEmbeddingV2'] as bool) {
906 907 908 909 910 911 912
          throwToolExit(
            'The plugin `${plugin['name']}` requires your app to be migrated to '
            'the Android embedding v2. Follow the steps on https://flutter.dev/go/android-project-migration '
            'and re-run this command.'
          );
        }
      }
913
      templateContent = _androidPluginRegistryTemplateOldEmbedding;
914
      break;
915
  }
916
  globals.printTrace('Generating $registryPath');
917 918 919 920
  _renderTemplateToFile(
    templateContent,
    templateContext,
    registryPath,
921
  );
922 923
}

924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980
/// Generates the Dart plugin registrant, which allows to bind a platform
/// implementation of a Dart only plugin to its interface.
/// The new entrypoint wraps [currentMainUri], adds a [_registerPlugins] function,
/// and writes the file to [newMainDart].
///
/// [mainFile] is the main entrypoint file. e.g. /<app>/lib/main.dart.
///
/// Returns [true] if it's necessary to create a plugin registrant, and
/// if the new entrypoint was written to disk.
///
/// For more details, see https://flutter.dev/go/federated-plugins.
Future<bool> generateMainDartWithPluginRegistrant(
  FlutterProject rootProject,
  PackageConfig packageConfig,
  String currentMainUri,
  File newMainDart,
  File mainFile,
) async {
  final List<Plugin> plugins = await findPlugins(rootProject);
  final List<PluginInterfaceResolution> resolutions = resolvePlatformImplementation(
    plugins,
    // TODO(egarciad): Turn this on after fixing the pubspec.yaml of the plugins used in tests.
    throwOnPluginPubspecError: false,
  );
  final LanguageVersion entrypointVersion = determineLanguageVersion(
    mainFile,
    packageConfig.packageOf(mainFile.absolute.uri),
  );
  final Map<String, dynamic> templateContext = <String, dynamic>{
    'mainEntrypoint': currentMainUri,
    'dartLanguageVersion': entrypointVersion.toString(),
    LinuxPlugin.kConfigKey: <dynamic>[],
    MacOSPlugin.kConfigKey: <dynamic>[],
    WindowsPlugin.kConfigKey: <dynamic>[],
  };
  bool didFindPlugin = false;
  for (final PluginInterfaceResolution resolution in resolutions) {
    assert(templateContext.containsKey(resolution.platform));
    (templateContext[resolution.platform] as List<dynamic>).add(resolution.toMap());
    didFindPlugin = true;
  }
  if (!didFindPlugin) {
    return false;
  }
  try {
    _renderTemplateToFile(
      _dartPluginRegistryForDesktopTemplate,
      templateContext,
      newMainDart.path,
    );
    return true;
  } on FileSystemException catch (error) {
    throwToolExit('Unable to write ${newMainDart.path}, received error: $error');
    return false;
  }
}

981 982
const String _objcPluginRegistryHeaderTemplate = '''
//
983 984 985
//  Generated file. Do not edit.
//

986 987
#ifndef GeneratedPluginRegistrant_h
#define GeneratedPluginRegistrant_h
988

989
#import <{{framework}}/{{framework}}.h>
990

991 992
NS_ASSUME_NONNULL_BEGIN

993 994
@interface GeneratedPluginRegistrant : NSObject
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
995 996
@end

997
NS_ASSUME_NONNULL_END
998
#endif /* GeneratedPluginRegistrant_h */
999 1000
''';

1001 1002
const String _objcPluginRegistryImplementationTemplate = '''
//
1003 1004 1005
//  Generated file. Do not edit.
//

1006
#import "GeneratedPluginRegistrant.h"
1007

1008
{{#plugins}}
1009
#if __has_include(<{{name}}/{{class}}.h>)
1010
#import <{{name}}/{{class}}.h>
1011 1012 1013
#else
@import {{name}};
#endif
1014

1015
{{/plugins}}
1016
@implementation GeneratedPluginRegistrant
1017

1018
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
1019
{{#plugins}}
1020
  [{{prefix}}{{class}} registerWithRegistrar:[registry registrarForPlugin:@"{{prefix}}{{class}}"]];
1021 1022 1023 1024 1025 1026
{{/plugins}}
}

@end
''';

1027 1028
const String _swiftPluginRegistryTemplate = '''
//
1029 1030
//  Generated file. Do not edit.
//
1031

1032
import {{framework}}
1033
import Foundation
1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045

{{#plugins}}
import {{name}}
{{/plugins}}

func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
  {{#plugins}}
  {{class}}.register(with: registry.registrar(forPlugin: "{{class}}"))
{{/plugins}}
}
''';

1046
const String _pluginRegistrantPodspecTemplate = '''
1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057
#
# Generated file, do not edit.
#

Pod::Spec.new do |s|
  s.name             = 'FlutterPluginRegistrant'
  s.version          = '0.0.1'
  s.summary          = 'Registers plugins with your flutter app'
  s.description      = <<-DESC
Depends on all your plugins, and provides a function to register them.
                       DESC
1058
  s.homepage         = 'https://flutter.dev'
1059 1060
  s.license          = { :type => 'BSD' }
  s.author           = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
1061
  s.{{os}}.deployment_target = '{{deploymentTarget}}'
1062 1063 1064
  s.source_files =  "Classes", "Classes/**/*.{h,m}"
  s.source           = { :path => '.' }
  s.public_header_files = './Classes/**/*.h'
1065
  s.static_framework    = true
1066
  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
1067
  s.dependency '{{framework}}'
1068 1069 1070 1071 1072 1073
  {{#plugins}}
  s.dependency '{{name}}'
  {{/plugins}}
end
''';

1074
const String _dartPluginRegistryForWebTemplate = '''
1075
//
1076 1077
// Generated file. Do not edit.
//
1078

1079 1080
// ignore_for_file: lines_longer_than_80_chars

1081 1082 1083 1084 1085 1086
{{#plugins}}
import 'package:{{name}}/{{file}}';
{{/plugins}}

import 'package:flutter_web_plugins/flutter_web_plugins.dart';

1087
// ignore: public_member_api_docs
1088
void registerPlugins(Registrar registrar) {
1089
{{#plugins}}
1090
  {{class}}.registerWith(registrar);
1091
{{/plugins}}
1092
  registrar.registerMessageHandler();
1093 1094 1095
}
''';

1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136
// TODO(egarciad): Evaluate merging the web and desktop plugin registry templates.
const String _dartPluginRegistryForDesktopTemplate = '''
//
// Generated file. Do not edit.
//

// @dart = {{dartLanguageVersion}}

import '{{mainEntrypoint}}' as entrypoint;
import 'dart:io'; // ignore: dart_io_import.
{{#linux}}
import 'package:{{pluginName}}/{{pluginName}}.dart';
{{/linux}}
{{#macos}}
import 'package:{{pluginName}}/{{pluginName}}.dart';
{{/macos}}
{{#windows}}
import 'package:{{pluginName}}/{{pluginName}}.dart';
{{/windows}}

@pragma('vm:entry-point')
void _registerPlugins() {
  if (Platform.isLinux) {
    {{#linux}}
      {{dartClass}}.registerWith();
    {{/linux}}
  } else if (Platform.isMacOS) {
    {{#macos}}
      {{dartClass}}.registerWith();
    {{/macos}}
  } else if (Platform.isWindows) {
    {{#windows}}
      {{dartClass}}.registerWith();
    {{/windows}}
  }
}
void main() {
  entrypoint.main();
}
''';

1137 1138
const String _cppPluginRegistryHeaderTemplate = '''
//
1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152
//  Generated file. Do not edit.
//

#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_

#include <flutter/plugin_registry.h>

// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);

#endif  // GENERATED_PLUGIN_REGISTRANT_
''';

1153 1154
const String _cppPluginRegistryImplementationTemplate = '''
//
1155 1156 1157 1158 1159 1160
//  Generated file. Do not edit.
//

#include "generated_plugin_registrant.h"

{{#plugins}}
1161
#include <{{name}}/{{filename}}.h>
1162 1163 1164 1165 1166 1167 1168 1169 1170 1171
{{/plugins}}

void RegisterPlugins(flutter::PluginRegistry* registry) {
{{#plugins}}
  {{class}}RegisterWithRegistrar(
      registry->GetRegistrarForPlugin("{{class}}"));
{{/plugins}}
}
''';

1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207
const String _linuxPluginRegistryHeaderTemplate = '''
//
//  Generated file. Do not edit.
//

#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_

#include <flutter_linux/flutter_linux.h>

// Registers Flutter plugins.
void fl_register_plugins(FlPluginRegistry* registry);

#endif  // GENERATED_PLUGIN_REGISTRANT_
''';

const String _linuxPluginRegistryImplementationTemplate = '''
//
//  Generated file. Do not edit.
//

#include "generated_plugin_registrant.h"

{{#plugins}}
#include <{{name}}/{{filename}}.h>
{{/plugins}}

void fl_register_plugins(FlPluginRegistry* registry) {
{{#plugins}}
  g_autoptr(FlPluginRegistrar) {{name}}_registrar =
      fl_plugin_registry_get_registrar_for_plugin(registry, "{{class}}");
  {{filename}}_register_with_registrar({{name}}_registrar);
{{/plugins}}
}
''';

1208
const String _pluginCmakefileTemplate = r'''
1209 1210 1211 1212
#
# Generated file, do not edit.
#

1213
list(APPEND FLUTTER_PLUGIN_LIST
1214
{{#plugins}}
1215
  {{name}}
1216
{{/plugins}}
1217
)
1218

1219 1220
set(PLUGIN_BUNDLED_LIBRARIES)

1221
foreach(plugin ${FLUTTER_PLUGIN_LIST})
1222
  add_subdirectory({{pluginsDir}}/${plugin}/{{os}} plugins/${plugin})
1223
  target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
1224 1225
  list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
  list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
1226
endforeach(plugin)
1227 1228
''';

1229
Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
1230
  final List<Map<String, dynamic>> iosPlugins = _extractPlatformMaps(plugins, IOSPlugin.kConfigKey);
1231
  final Map<String, dynamic> context = <String, dynamic>{
1232
    'os': 'ios',
1233
    'deploymentTarget': '9.0',
1234
    'framework': 'Flutter',
1235 1236
    'plugins': iosPlugins,
  };
1237
  if (project.isModule) {
1238
    final String registryDirectory = project.ios.pluginRegistrantHost.path;
1239
    _renderTemplateToFile(
1240
      _pluginRegistrantPodspecTemplate,
1241
      context,
1242
      globals.fs.path.join(registryDirectory, 'FlutterPluginRegistrant.podspec'),
1243 1244
    );
  }
1245 1246 1247 1248 1249 1250 1251 1252 1253 1254
  _renderTemplateToFile(
    _objcPluginRegistryHeaderTemplate,
    context,
    project.ios.pluginRegistrantHeader.path,
  );
  _renderTemplateToFile(
    _objcPluginRegistryImplementationTemplate,
    context,
    project.ios.pluginRegistrantImplementation.path,
  );
1255 1256
}

1257 1258 1259 1260 1261 1262 1263 1264
/// The relative path from a project's main CMake file to the plugin symlink
/// directory to use in the generated plugin CMake file.
///
/// Because the generated file is checked in, it can't use absolute paths. It is
/// designed to be included by the main CMakeLists.txt, so it relative to
/// that file, rather than the generated file.
String _cmakeRelativePluginSymlinkDirectoryPath(CmakeBasedProject project) {
  final String makefileDirPath = project.cmakeFile.parent.absolute.path;
1265
  // CMake always uses posix-style path separators, regardless of the platform.
1266 1267 1268 1269 1270 1271 1272 1273
  final path.Context cmakePathContext = path.Context(style: path.Style.posix);
  final List<String> relativePathComponents = globals.fs.path.split(globals.fs.path.relative(
    project.pluginSymlinkDirectory.absolute.path,
    from: makefileDirPath,
  ));
  return cmakePathContext.joinAll(relativePathComponents);
}

1274
Future<void> _writeLinuxPluginFiles(FlutterProject project, List<Plugin> plugins) async {
1275 1276
  final List<Plugin>nativePlugins = _filterNativePlugins(plugins, LinuxPlugin.kConfigKey);
  final List<Map<String, dynamic>> linuxPlugins = _extractPlatformMaps(nativePlugins, LinuxPlugin.kConfigKey);
1277
  final Map<String, dynamic> context = <String, dynamic>{
1278
    'os': 'linux',
1279
    'plugins': linuxPlugins,
1280
    'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.linux),
1281
  };
1282
  await _writeLinuxPluginRegistrant(project.linux.managedDirectory, context);
1283
  await _writePluginCmakefile(project.linux.generatedPluginCmakeFile, context);
1284 1285
}

1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299
Future<void> _writeLinuxPluginRegistrant(Directory destination, Map<String, dynamic> templateContext) async {
  final String registryDirectory = destination.path;
  _renderTemplateToFile(
    _linuxPluginRegistryHeaderTemplate,
    templateContext,
    globals.fs.path.join(registryDirectory, 'generated_plugin_registrant.h'),
  );
  _renderTemplateToFile(
    _linuxPluginRegistryImplementationTemplate,
    templateContext,
    globals.fs.path.join(registryDirectory, 'generated_plugin_registrant.cc'),
  );
}

1300
Future<void> _writePluginCmakefile(File destinationFile, Map<String, dynamic> templateContext) async {
1301
  _renderTemplateToFile(
1302
    _pluginCmakefileTemplate,
1303
    templateContext,
1304
    destinationFile.path,
1305
  );
1306 1307
}

1308
Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
1309 1310
  final List<Plugin>nativePlugins = _filterNativePlugins(plugins, MacOSPlugin.kConfigKey);
  final List<Map<String, dynamic>> macosPlugins = _extractPlatformMaps(nativePlugins, MacOSPlugin.kConfigKey);
1311 1312 1313 1314 1315 1316 1317 1318 1319
  final Map<String, dynamic> context = <String, dynamic>{
    'os': 'macos',
    'framework': 'FlutterMacOS',
    'plugins': macosPlugins,
  };
  final String registryDirectory = project.macos.managedDirectory.path;
  _renderTemplateToFile(
    _swiftPluginRegistryTemplate,
    context,
1320
    globals.fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.swift'),
1321 1322 1323
  );
}

1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339
/// Filters out Dart-only plugins, which shouldn't be added to the native generated registrants.
List<Plugin> _filterNativePlugins(List<Plugin> plugins, String platformKey) {
  return plugins.where((Plugin element) {
    final PluginPlatform plugin = element.platforms[platformKey];
    if (plugin == null) {
      return false;
    }
    if (plugin is NativeOrDartPlugin) {
      return (plugin as NativeOrDartPlugin).isNative();
    }
    // Not all platforms have the ability to create Dart-only plugins. Therefore, any plugin that doesn't
    // implement NativeOrDartPlugin is always native.
    return true;
  }).toList();
}

1340
Future<void> _writeWindowsPluginFiles(FlutterProject project, List<Plugin> plugins) async {
1341 1342
  final List<Plugin>nativePlugins = _filterNativePlugins(plugins, WindowsPlugin.kConfigKey);
  final List<Map<String, dynamic>> windowsPlugins = _extractPlatformMaps(nativePlugins, WindowsPlugin.kConfigKey);
1343
  final Map<String, dynamic> context = <String, dynamic>{
1344
    'os': 'windows',
1345
    'plugins': windowsPlugins,
1346
    'pluginsDir': _cmakeRelativePluginSymlinkDirectoryPath(project.windows),
1347 1348
  };
  await _writeCppPluginRegistrant(project.windows.managedDirectory, context);
1349
  await _writePluginCmakefile(project.windows.generatedPluginCmakeFile, context);
1350 1351 1352 1353 1354 1355 1356
}

Future<void> _writeCppPluginRegistrant(Directory destination, Map<String, dynamic> templateContext) async {
  final String registryDirectory = destination.path;
  _renderTemplateToFile(
    _cppPluginRegistryHeaderTemplate,
    templateContext,
1357
    globals.fs.path.join(registryDirectory, 'generated_plugin_registrant.h'),
1358 1359 1360 1361
  );
  _renderTemplateToFile(
    _cppPluginRegistryImplementationTemplate,
    templateContext,
1362
    globals.fs.path.join(registryDirectory, 'generated_plugin_registrant.cc'),
1363 1364 1365
  );
}

1366 1367 1368 1369 1370 1371
Future<void> _writeWebPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
  final List<Map<String, dynamic>> webPlugins = _extractPlatformMaps(plugins, WebPlugin.kConfigKey);
  final Map<String, dynamic> context = <String, dynamic>{
    'plugins': webPlugins,
  };
  final String registryDirectory = project.web.libDirectory.path;
1372
  final String filePath = globals.fs.path.join(registryDirectory, 'generated_plugin_registrant.dart');
1373
  if (webPlugins.isEmpty) {
1374
    final File file = globals.fs.file(filePath);
1375
    return ErrorHandlingFileSystem.deleteIfExists(file);
1376 1377
  } else {
    _renderTemplateToFile(
1378
      _dartPluginRegistryForWebTemplate,
1379 1380 1381 1382 1383 1384
      context,
      filePath,
    );
  }
}

1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417
/// For each platform that uses them, creates symlinks within the platform
/// directory to each plugin used on that platform.
///
/// If |force| is true, the symlinks will be recreated, otherwise they will
/// be created only if missing.
///
/// This uses [project.flutterPluginsDependenciesFile], so it should only be
/// run after refreshPluginList has been run since the last plugin change.
void createPluginSymlinks(FlutterProject project, {bool force = false}) {
  Map<String, dynamic> platformPlugins;
  final String pluginFileContent = _readFileContent(project.flutterPluginsDependenciesFile);
  if (pluginFileContent != null) {
    final Map<String, dynamic> pluginInfo = json.decode(pluginFileContent) as Map<String, dynamic>;
    platformPlugins = pluginInfo[_kFlutterPluginsPluginListKey] as Map<String, dynamic>;
  }
  platformPlugins ??= <String, dynamic>{};

  if (featureFlags.isWindowsEnabled && project.windows.existsSync()) {
    _createPlatformPluginSymlinks(
      project.windows.pluginSymlinkDirectory,
      platformPlugins[project.windows.pluginConfigKey] as List<dynamic>,
      force: force,
    );
  }
  if (featureFlags.isLinuxEnabled && project.linux.existsSync()) {
    _createPlatformPluginSymlinks(
      project.linux.pluginSymlinkDirectory,
      platformPlugins[project.linux.pluginConfigKey] as List<dynamic>,
      force: force,
    );
  }
}

1418 1419 1420 1421 1422 1423 1424 1425 1426 1427
/// Handler for symlink failures which provides specific instructions for known
/// failure cases.
@visibleForTesting
void handleSymlinkException(FileSystemException e, {
  @required Platform platform,
  @required OperatingSystemUtils os,
}) {
  if (platform.isWindows && (e.osError?.errorCode ?? 0) == 1314) {
    final String versionString = RegExp(r'[\d.]+').firstMatch(os.name)?.group(0);
    final Version version = Version.parse(versionString);
1428
    // Windows 10 14972 is the oldest version that allows creating symlinks
1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441
    // just by enabling developer mode; before that it requires running the
    // terminal as Administrator.
    // https://blogs.windows.com/windowsdeveloper/2016/12/02/symlinks-windows-10/
    final String instructions = (version != null && version >= Version(10, 0, 14972))
        ? 'Please enable Developer Mode in your system settings. Run\n'
          '  start ms-settings:developers\n'
          'to open settings.'
        : 'You must build from a terminal run as administrator.';
    throwToolExit('Building with plugins requires symlink support.\n\n' +
        instructions);
  }
}

1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463
/// Creates [symlinkDirectory] containing symlinks to each plugin listed in [platformPlugins].
///
/// If [force] is true, the directory will be created only if missing.
void _createPlatformPluginSymlinks(Directory symlinkDirectory, List<dynamic> platformPlugins, {bool force = false}) {
  if (force && symlinkDirectory.existsSync()) {
    // Start fresh to avoid stale links.
    symlinkDirectory.deleteSync(recursive: true);
  }
  symlinkDirectory.createSync(recursive: true);
  if (platformPlugins == null) {
    return;
  }
  for (final Map<String, dynamic> pluginInfo in platformPlugins.cast<Map<String, dynamic>>()) {
    final String name = pluginInfo[_kFlutterPluginsNameKey] as String;
    final String path = pluginInfo[_kFlutterPluginsPathKey] as String;
    final Link link = symlinkDirectory.childLink(name);
    if (link.existsSync()) {
      continue;
    }
    try {
      link.createSync(path);
    } on FileSystemException catch (e) {
1464
      handleSymlinkException(e, platform: globals.platform, os: globals.os);
1465 1466 1467 1468 1469
      rethrow;
    }
  }
}

1470 1471 1472 1473
/// Rewrites the `.flutter-plugins` file of [project] based on the plugin
/// dependencies declared in `pubspec.yaml`.
///
/// Assumes `pub get` has been executed since last change to `pubspec.yaml`.
1474 1475 1476 1477 1478
Future<void> refreshPluginsList(
  FlutterProject project, {
  bool iosPlatform = false,
  bool macOSPlatform = false,
}) async {
1479
  final List<Plugin> plugins = await findPlugins(project);
1480 1481
  // Sort the plugins by name to keep ordering stable in generated files.
  plugins.sort((Plugin left, Plugin right) => left.name.compareTo(right.name));
1482 1483 1484 1485
  // TODO(franciscojma): Remove once migration is complete.
  // Write the legacy plugin files to avoid breaking existing apps.
  final bool legacyChanged = _writeFlutterPluginsListLegacy(project, plugins);

1486
  final bool changed = _writeFlutterPluginsList(project, plugins);
1487
  if (changed || legacyChanged) {
1488
    createPluginSymlinks(project, force: true);
1489
    if (iosPlatform) {
1490
      globals.cocoaPods.invalidatePodInstallOutput(project.ios);
1491
    }
1492
    if (macOSPlatform) {
1493
      globals.cocoaPods.invalidatePodInstallOutput(project.macos);
1494 1495
    }
  }
1496 1497
}

1498
/// Injects plugins found in `pubspec.yaml` into the platform-specific projects.
1499 1500
///
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
1501 1502 1503 1504 1505 1506 1507 1508 1509
Future<void> injectPlugins(
  FlutterProject project, {
  bool androidPlatform = false,
  bool iosPlatform = false,
  bool linuxPlatform = false,
  bool macOSPlatform = false,
  bool windowsPlatform = false,
  bool webPlatform = false,
}) async {
1510
  final List<Plugin> plugins = await findPlugins(project);
1511 1512
  // Sort the plugins by name to keep ordering stable in generated files.
  plugins.sort((Plugin left, Plugin right) => left.name.compareTo(right.name));
1513
  if (androidPlatform) {
1514 1515
    await _writeAndroidPluginRegistrant(project, plugins);
  }
1516
  if (iosPlatform) {
1517 1518
    await _writeIOSPluginRegistrant(project, plugins);
  }
1519
  if (linuxPlatform) {
1520
    await _writeLinuxPluginFiles(project, plugins);
1521
  }
1522
  if (macOSPlatform) {
1523
    await _writeMacOSPluginRegistrant(project, plugins);
1524
  }
1525
  if (windowsPlatform) {
1526
    await _writeWindowsPluginFiles(project, plugins);
1527
  }
1528 1529 1530 1531 1532 1533
  if (!project.isModule) {
    final List<XcodeBasedProject> darwinProjects = <XcodeBasedProject>[
      if (iosPlatform) project.ios,
      if (macOSPlatform) project.macos,
    ];
    for (final XcodeBasedProject subproject in darwinProjects) {
1534
      if (plugins.isNotEmpty) {
1535
        await globals.cocoaPods.setupPodfile(subproject);
1536 1537 1538 1539
      }
      /// The user may have a custom maintained Podfile that they're running `pod install`
      /// on themselves.
      else if (subproject.podfile.existsSync() && subproject.podfileLock.existsSync()) {
1540
        globals.cocoaPods.addPodsDependencyToFlutterXcconfig(subproject);
1541
      }
1542
    }
1543
  }
1544
  if (webPlatform) {
1545 1546
    await _writeWebPluginRegistrant(project, plugins);
  }
1547 1548
}

1549
/// Returns whether the specified Flutter [project] has any plugin dependencies.
1550 1551
///
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
1552
bool hasPlugins(FlutterProject project) {
1553
  return _readFileContent(project.flutterPluginsFile) != null;
1554
}