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

5 6
import 'dart:async';

7
import 'package:meta/meta.dart';
8
import 'package:mustache/mustache.dart' as mustache;
9 10
import 'package:yaml/yaml.dart';

11
import 'android/gradle.dart';
12
import 'base/common.dart';
13
import 'base/file_system.dart';
14
import 'convert.dart';
15
import 'dart/package_map.dart';
16
import 'features.dart';
17
import 'globals.dart';
18
import 'macos/cocoapods.dart';
19
import 'platform_plugins.dart';
20
import 'project.dart';
21

22 23
void _renderTemplateToFile(String template, dynamic context, String filePath) {
  final String renderedTemplate =
24
     mustache.Template(template, htmlEscapeValues: false).renderString(context);
25 26 27 28 29
  final File file = fs.file(filePath);
  file.createSync(recursive: true);
  file.writeAsStringSync(renderedTemplate);
}

30
class Plugin {
31
  Plugin({
32 33 34 35 36 37 38 39
    @required this.name,
    @required this.path,
    @required this.platforms,
    @required this.dependencies,
  }) : assert(name != null),
       assert(path != null),
       assert(platforms != null),
       assert(dependencies != null);
40

41 42 43
  /// Parses [Plugin] specification from the provided pluginYaml.
  ///
  /// This currently supports two formats. Legacy and Multi-platform.
44
  ///
45 46
  /// Example of the deprecated Legacy format.
  ///
47 48 49 50
  ///     flutter:
  ///      plugin:
  ///        androidPackage: io.flutter.plugins.sample
  ///        iosPrefix: FLT
51
  ///        pluginClass: SamplePlugin
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
  ///
  /// 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
69 70 71 72 73 74
  factory Plugin.fromYaml(
    String name,
    String path,
    YamlMap pluginYaml,
    List<String> dependencies,
  ) {
75 76 77 78 79
    final List<String> errors = validatePluginYaml(pluginYaml);
    if (errors.isNotEmpty) {
      throwToolExit('Invalid plugin specification.\n${errors.join('\n')}');
    }
    if (pluginYaml != null && pluginYaml['platforms'] != null) {
80
      return Plugin._fromMultiPlatformYaml(name, path, pluginYaml, dependencies);
81
    }
82
    return Plugin._fromLegacyYaml(name, path, pluginYaml, dependencies);
83 84
  }

85 86 87 88 89 90
  factory Plugin._fromMultiPlatformYaml(
    String name,
    String path,
    dynamic pluginYaml,
    List<String> dependencies,
  ) {
91 92
    assert (pluginYaml != null && pluginYaml['platforms'] != null,
            'Invalid multi-platform plugin specification.');
93
    final YamlMap platformsYaml = pluginYaml['platforms'] as YamlMap;
94 95 96 97 98 99

    assert (_validateMultiPlatformYaml(platformsYaml).isEmpty,
            'Invalid multi-platform plugin specification.');

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

100
    if (_providesImplementationForPlatform(platformsYaml, AndroidPlugin.kConfigKey)) {
101 102
      platforms[AndroidPlugin.kConfigKey] = AndroidPlugin.fromYaml(
        name,
103
        platformsYaml[AndroidPlugin.kConfigKey] as YamlMap,
104 105
        path,
      );
106 107
    }

108
    if (_providesImplementationForPlatform(platformsYaml, IOSPlugin.kConfigKey)) {
109
      platforms[IOSPlugin.kConfigKey] =
110
          IOSPlugin.fromYaml(name, platformsYaml[IOSPlugin.kConfigKey] as YamlMap);
111 112
    }

113
    if (_providesImplementationForPlatform(platformsYaml, LinuxPlugin.kConfigKey)) {
114
      platforms[LinuxPlugin.kConfigKey] =
115
          LinuxPlugin.fromYaml(name, platformsYaml[LinuxPlugin.kConfigKey] as YamlMap);
116 117
    }

118
    if (_providesImplementationForPlatform(platformsYaml, MacOSPlugin.kConfigKey)) {
119
      platforms[MacOSPlugin.kConfigKey] =
120
          MacOSPlugin.fromYaml(name, platformsYaml[MacOSPlugin.kConfigKey] as YamlMap);
121 122
    }

123
    if (_providesImplementationForPlatform(platformsYaml, WebPlugin.kConfigKey)) {
124
      platforms[WebPlugin.kConfigKey] =
125
          WebPlugin.fromYaml(name, platformsYaml[WebPlugin.kConfigKey] as YamlMap);
126 127
    }

128
    if (_providesImplementationForPlatform(platformsYaml, WindowsPlugin.kConfigKey)) {
129
      platforms[WindowsPlugin.kConfigKey] =
130
          WindowsPlugin.fromYaml(name, platformsYaml[WindowsPlugin.kConfigKey] as YamlMap);
131 132
    }

133 134 135 136
    return Plugin(
      name: name,
      path: path,
      platforms: platforms,
137
      dependencies: dependencies,
138 139 140
    );
  }

141 142 143 144 145 146
  factory Plugin._fromLegacyYaml(
    String name,
    String path,
    dynamic pluginYaml,
    List<String> dependencies,
  ) {
147
    final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
148
    final String pluginClass = pluginYaml['pluginClass'] as String;
149
    if (pluginYaml != null && pluginClass != null) {
150
      final String androidPackage = pluginYaml['androidPackage'] as String;
151
      if (androidPackage != null) {
152 153
        platforms[AndroidPlugin.kConfigKey] = AndroidPlugin(
          name: name,
154
          package: pluginYaml['androidPackage'] as String,
155 156 157
          pluginClass: pluginClass,
          pluginPath: path,
        );
158 159
      }

160
      final String iosPrefix = pluginYaml['iosPrefix'] as String ?? '';
161 162 163 164 165 166
      platforms[IOSPlugin.kConfigKey] =
          IOSPlugin(
            name: name,
            classPrefix: iosPrefix,
            pluginClass: pluginClass,
          );
167
    }
168
    return Plugin(
169 170
      name: name,
      path: path,
171
      platforms: platforms,
172
      dependencies: dependencies,
173
    );
174
  }
175

176
  static List<String> validatePluginYaml(YamlMap yaml) {
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195

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

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

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

    if (usesNewPluginFormat) {
      return _validateMultiPlatformYaml(yaml['platforms'] as YamlMap);
196 197 198 199 200 201
    } else {
      return _validateLegacyYaml(yaml);
    }
  }

  static List<String> _validateMultiPlatformYaml(YamlMap yaml) {
202 203
    bool isInvalid(String key, bool Function(YamlMap) validate) {
      final dynamic value = yaml[key];
204 205 206
      if (!(value is YamlMap)) {
        return false;
      }
207 208
      final YamlMap yamlValue = value as YamlMap;
      if (yamlValue.containsKey('default_package')) {
209 210
        return false;
      }
211
      return !validate(yamlValue);
212
    }
213
    final List<String> errors = <String>[];
214
    if (isInvalid(AndroidPlugin.kConfigKey, AndroidPlugin.validate)) {
215 216
      errors.add('Invalid "android" plugin specification.');
    }
217
    if (isInvalid(IOSPlugin.kConfigKey, IOSPlugin.validate)) {
218 219
      errors.add('Invalid "ios" plugin specification.');
    }
220
    if (isInvalid(LinuxPlugin.kConfigKey, LinuxPlugin.validate)) {
221 222
      errors.add('Invalid "linux" plugin specification.');
    }
223
    if (isInvalid(MacOSPlugin.kConfigKey, MacOSPlugin.validate)) {
224 225
      errors.add('Invalid "macos" plugin specification.');
    }
226
    if (isInvalid(WindowsPlugin.kConfigKey, WindowsPlugin.validate)) {
227 228
      errors.add('Invalid "windows" plugin specification.');
    }
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
    return errors;
  }

  static List<String> _validateLegacyYaml(YamlMap yaml) {
    final List<String> errors = <String>[];
    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;
  }

246 247 248 249
  static bool _providesImplementationForPlatform(YamlMap platformsYaml, String platformKey) {
    if (!platformsYaml.containsKey(platformKey)) {
      return false;
    }
250
    if ((platformsYaml[platformKey] as YamlMap).containsKey('default_package')) {
251 252 253 254 255
      return false;
    }
    return true;
  }

256 257
  final String name;
  final String path;
258

259 260 261
  /// The name of the packages this plugin depends on.
  final List<String> dependencies;

262 263
  /// This is a mapping from platform config key to the plugin platform spec.
  final Map<String, PluginPlatform> platforms;
264 265 266
}

Plugin _pluginFromPubspec(String name, Uri packageRoot) {
267
  final String pubspecPath = fs.path.fromUri(packageRoot.resolve('pubspec.yaml'));
268
  if (!fs.isFileSync(pubspecPath)) {
269
    return null;
270
  }
271
  final dynamic pubspec = loadYaml(fs.file(pubspecPath).readAsStringSync());
272
  if (pubspec == null) {
273
    return null;
274
  }
275
  final dynamic flutterConfig = pubspec['flutter'];
276
  if (flutterConfig == null || !(flutterConfig.containsKey('plugin') as bool)) {
277
    return null;
278
  }
279
  final String packageRootPath = fs.path.fromUri(packageRoot);
280
  final YamlMap dependencies = pubspec['dependencies'] as YamlMap;
281
  printTrace('Found plugin $name at $packageRootPath');
282 283 284
  return Plugin.fromYaml(
    name,
    packageRootPath,
285
    flutterConfig['plugin'] as YamlMap,
286
    dependencies == null ? <String>[] : <String>[...dependencies.keys.cast<String>()],
287
  );
288 289
}

290
List<Plugin> findPlugins(FlutterProject project) {
291
  final List<Plugin> plugins = <Plugin>[];
292 293
  Map<String, Uri> packages;
  try {
294 295 296 297
    final String packagesFile = fs.path.join(
      project.directory.path,
      PackageMap.globalPackagesPath,
    );
298
    packages = PackageMap(packagesFile).map;
299
  } on FormatException catch (e) {
300
    printTrace('Invalid .packages file: $e');
301
    return plugins;
302 303 304
  }
  packages.forEach((String name, Uri uri) {
    final Uri packageRoot = uri.resolve('..');
305
    final Plugin plugin = _pluginFromPubspec(name, packageRoot);
306
    if (plugin != null) {
307
      plugins.add(plugin);
308
    }
309
  });
310
  return plugins;
311 312
}

313 314 315 316 317
/// Writes the .flutter-plugins and .flutter-plugins-dependencies 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 or .flutter-plugins-dependencies have changed,
/// otherwise returns [false].
318
bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) {
319
  final List<dynamic> directAppDependencies = <dynamic>[];
320 321
  const String info = 'This is a generated file; do not edit or check into version control.';
  final StringBuffer flutterPluginsBuffer = StringBuffer('# $info\n');
322 323 324 325 326 327 328 329 330 331 332 333 334

  final Set<String> pluginNames = <String>{};
  for (Plugin plugin in plugins) {
    pluginNames.add(plugin.name);
  }
  for (Plugin plugin in plugins) {
    flutterPluginsBuffer.write('${plugin.name}=${escapePath(plugin.path)}\n');
    directAppDependencies.add(<String, dynamic>{
      'name': plugin.name,
      // Extract the plugin dependencies which happen to be plugins.
      'dependencies': <String>[...plugin.dependencies.where(pluginNames.contains)],
    });
  }
335
  final File pluginsFile = project.flutterPluginsFile;
336 337
  final String oldPluginFileContent = _readFileContent(pluginsFile);
  final String pluginFileContent = flutterPluginsBuffer.toString();
338
  if (pluginNames.isNotEmpty) {
339
    pluginsFile.writeAsStringSync(pluginFileContent, flush: true);
340
  } else {
341
    if (pluginsFile.existsSync()) {
342
      pluginsFile.deleteSync();
343
    }
344
  }
345 346 347 348

  final File dependenciesFile = project.flutterPluginsDependenciesFile;
  final String oldDependenciesFileContent = _readFileContent(dependenciesFile);
  final String dependenciesFileContent = json.encode(<String, dynamic>{
349
      '_info': '// $info',
350 351
      'dependencyGraph': directAppDependencies,
    });
352
  if (pluginNames.isNotEmpty) {
353 354 355 356 357 358 359 360 361
    dependenciesFile.writeAsStringSync(dependenciesFileContent, flush: true);
  } else {
    if (dependenciesFile.existsSync()) {
      dependenciesFile.deleteSync();
    }
  }

  return oldPluginFileContent != _readFileContent(pluginsFile)
      || oldDependenciesFileContent != _readFileContent(dependenciesFile);
362 363
}

364 365 366
/// Returns the contents of [File] or [null] if that file does not exist.
String _readFileContent(File file) {
  return file.existsSync() ? file.readAsStringSync() : null;
367
}
368

369
const String _androidPluginRegistryTemplateOldEmbedding = '''package io.flutter.plugins;
370

371
import io.flutter.plugin.common.PluginRegistry;
372
{{#plugins}}
373 374 375 376 377 378
import {{package}}.{{class}};
{{/plugins}}

/**
 * Generated file. Do not edit.
 */
379 380
public final class GeneratedPluginRegistrant {
  public static void registerWith(PluginRegistry registry) {
381 382 383
    if (alreadyRegisteredWith(registry)) {
      return;
    }
384
{{#plugins}}
385
    {{class}}.registerWith(registry.registrarFor("{{package}}.{{class}}"));
386
{{/plugins}}
387
  }
388 389 390 391 392 393 394 395 396

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

400
const String _androidPluginRegistryTemplateNewEmbedding = '''package io.flutter.plugins;
401 402

{{#androidX}}
403
import androidx.annotation.Keep;
404 405 406
import androidx.annotation.NonNull;
{{/androidX}}
{{^androidX}}
407
import android.support.annotation.Keep;
408 409 410 411 412 413 414 415 416 417 418 419
import android.support.annotation.NonNull;
{{/androidX}}
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.
 */
420
@Keep
421 422 423 424 425 426
public final class GeneratedPluginRegistrant {
  public static void registerWith(@NonNull FlutterEngine flutterEngine) {
{{#needsShim}}
    ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine);
{{/needsShim}}
{{#plugins}}
427
  {{#supportsEmbeddingV2}}
428
    flutterEngine.getPlugins().add(new {{package}}.{{class}}());
429 430 431 432 433 434
  {{/supportsEmbeddingV2}}
  {{^supportsEmbeddingV2}}
    {{#supportsEmbeddingV1}}
      {{package}}.{{class}}.registerWith(shimPluginRegistry.registrarFor("{{package}}.{{class}}"));
    {{/supportsEmbeddingV1}}
  {{/supportsEmbeddingV2}}
435 436 437 438 439
{{/plugins}}
  }
}
''';

440 441 442 443 444 445 446 447 448 449 450
List<Map<String, dynamic>> _extractPlatformMaps(List<Plugin> plugins, String type) {
  final List<Map<String, dynamic>> pluginConfigs = <Map<String, dynamic>>[];
  for (Plugin p in plugins) {
    final PluginPlatform platformPlugin = p.platforms[type];
    if (platformPlugin != null) {
      pluginConfigs.add(platformPlugin.toMap());
    }
  }
  return pluginConfigs;
}

451 452
/// Returns the version of the Android embedding that the current
/// [project] is using.
453
AndroidEmbeddingVersion _getAndroidEmbeddingVersion(FlutterProject project) {
454
  assert(project.android != null);
455

456
  return project.android.getEmbeddingVersion();
457 458
}

459
Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
460 461 462 463
  final List<Map<String, dynamic>> androidPlugins =
    _extractPlatformMaps(plugins, AndroidPlugin.kConfigKey);

  final Map<String, dynamic> templateContext = <String, dynamic>{
464
    'plugins': androidPlugins,
465
    'androidX': isAppUsingAndroidX(project.android.hostAppGradleRoot),
466
  };
467
  final String javaSourcePath = fs.path.join(
468
    project.android.pluginRegistrantHost.path,
469 470 471 472
    'src',
    'main',
    'java',
  );
473 474 475 476 477 478 479
  final String registryPath = fs.path.join(
    javaSourcePath,
    'io',
    'flutter',
    'plugins',
    'GeneratedPluginRegistrant.java',
  );
480
  String templateContent;
481
  final AndroidEmbeddingVersion appEmbeddingVersion = _getAndroidEmbeddingVersion(project);
482
  switch (appEmbeddingVersion) {
483
    case AndroidEmbeddingVersion.v2:
484 485
      templateContext['needsShim'] = false;
      // If a plugin is using an embedding version older than 2.0 and the app is using 2.0,
486
      // then add shim for the old plugins.
487
      for (Map<String, dynamic> plugin in androidPlugins) {
488
        if (plugin['supportsEmbeddingV1'] as bool && !(plugin['supportsEmbeddingV2'] as bool)) {
489
          templateContext['needsShim'] = true;
490 491 492 493 494 495 496 497 498 499
          if (project.isModule) {
            printStatus(
              '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.'
            );
          }
500 501 502 503
          break;
        }
      }
      templateContent = _androidPluginRegistryTemplateNewEmbedding;
504 505
      break;
    case AndroidEmbeddingVersion.v1:
506
    default:
507
      for (Map<String, dynamic> plugin in androidPlugins) {
508
        if (!(plugin['supportsEmbeddingV1'] as bool) && plugin['supportsEmbeddingV2'] as bool) {
509 510 511 512 513 514 515
          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.'
          );
        }
      }
516
      templateContent = _androidPluginRegistryTemplateOldEmbedding;
517
      break;
518 519 520 521 522 523
  }
  printTrace('Generating $registryPath');
  _renderTemplateToFile(
    templateContent,
    templateContext,
    registryPath,
524
  );
525 526
}

527
const String _objcPluginRegistryHeaderTemplate = '''//
528 529 530
//  Generated file. Do not edit.
//

531 532
#ifndef GeneratedPluginRegistrant_h
#define GeneratedPluginRegistrant_h
533

534
#import <{{framework}}/{{framework}}.h>
535

536 537
NS_ASSUME_NONNULL_BEGIN

538 539
@interface GeneratedPluginRegistrant : NSObject
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
540 541
@end

542
NS_ASSUME_NONNULL_END
543
#endif /* GeneratedPluginRegistrant_h */
544 545
''';

546
const String _objcPluginRegistryImplementationTemplate = '''//
547 548 549
//  Generated file. Do not edit.
//

550
#import "GeneratedPluginRegistrant.h"
551

552
{{#plugins}}
553
#if __has_include(<{{name}}/{{class}}.h>)
554
#import <{{name}}/{{class}}.h>
555 556 557
#else
@import {{name}};
#endif
558

559
{{/plugins}}
560
@implementation GeneratedPluginRegistrant
561

562
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
563
{{#plugins}}
564
  [{{prefix}}{{class}} registerWithRegistrar:[registry registrarForPlugin:@"{{prefix}}{{class}}"]];
565 566 567 568 569 570
{{/plugins}}
}

@end
''';

571 572 573
const String _swiftPluginRegistryTemplate = '''//
//  Generated file. Do not edit.
//
574

575
import {{framework}}
576
import Foundation
577 578 579 580 581 582 583 584 585 586 587 588

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

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

589
const String _pluginRegistrantPodspecTemplate = '''
590 591 592 593 594 595 596 597 598 599 600
#
# 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
601
  s.homepage         = 'https://flutter.dev'
602 603
  s.license          = { :type => 'BSD' }
  s.author           = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
604
  s.{{os}}.deployment_target = '{{deploymentTarget}}'
605 606 607
  s.source_files =  "Classes", "Classes/**/*.{h,m}"
  s.source           = { :path => '.' }
  s.public_header_files = './Classes/**/*.h'
608
  s.static_framework    = true
609
  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
610
  s.dependency '{{framework}}'
611 612 613 614 615 616
  {{#plugins}}
  s.dependency '{{name}}'
  {{/plugins}}
end
''';

617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
const String _dartPluginRegistryTemplate = '''//
// Generated file. Do not edit.
//
import 'dart:ui';

{{#plugins}}
import 'package:{{name}}/{{file}}';
{{/plugins}}

import 'package:flutter_web_plugins/flutter_web_plugins.dart';

void registerPlugins(PluginRegistry registry) {
{{#plugins}}
  {{class}}.registerWith(registry.registrarFor({{class}}));
{{/plugins}}
  registry.registerMessageHandler();
}
''';

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
const String _cppPluginRegistryHeaderTemplate = '''//
//  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_
''';

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

#include "generated_plugin_registrant.h"

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

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

669
Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
670
  final List<Map<String, dynamic>> iosPlugins = _extractPlatformMaps(plugins, IOSPlugin.kConfigKey);
671
  final Map<String, dynamic> context = <String, dynamic>{
672 673 674
    'os': 'ios',
    'deploymentTarget': '8.0',
    'framework': 'Flutter',
675 676
    'plugins': iosPlugins,
  };
677
  final String registryDirectory = project.ios.pluginRegistrantHost.path;
678
  if (project.isModule) {
679 680
    final String registryClassesDirectory = fs.path.join(registryDirectory, 'Classes');
    _renderTemplateToFile(
681
      _pluginRegistrantPodspecTemplate,
682
      context,
683 684 685
      fs.path.join(registryDirectory, 'FlutterPluginRegistrant.podspec'),
    );
    _renderTemplateToFile(
686 687
      _objcPluginRegistryHeaderTemplate,
      context,
688 689 690
      fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.h'),
    );
    _renderTemplateToFile(
691 692
      _objcPluginRegistryImplementationTemplate,
      context,
693 694 695 696
      fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.m'),
    );
  } else {
    _renderTemplateToFile(
697 698
      _objcPluginRegistryHeaderTemplate,
      context,
699
      fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.h'),
700 701
    );
    _renderTemplateToFile(
702 703
      _objcPluginRegistryImplementationTemplate,
      context,
704
      fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.m'),
705 706
    );
  }
707 708
}

709 710 711 712 713 714 715 716
Future<void> _writeLinuxPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
  final List<Map<String, dynamic>> linuxPlugins = _extractPlatformMaps(plugins, LinuxPlugin.kConfigKey);
  final Map<String, dynamic> context = <String, dynamic>{
    'plugins': linuxPlugins,
  };
  await _writeCppPluginRegistrant(project.linux.managedDirectory, context);
}

717
Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
718
  final List<Map<String, dynamic>> macosPlugins = _extractPlatformMaps(plugins, MacOSPlugin.kConfigKey);
719 720 721 722 723 724 725 726 727 728 729 730 731
  final Map<String, dynamic> context = <String, dynamic>{
    'os': 'macos',
    'framework': 'FlutterMacOS',
    'plugins': macosPlugins,
  };
  final String registryDirectory = project.macos.managedDirectory.path;
  _renderTemplateToFile(
    _swiftPluginRegistryTemplate,
    context,
    fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.swift'),
  );
}

732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753
Future<void> _writeWindowsPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
  final List<Map<String, dynamic>> windowsPlugins = _extractPlatformMaps(plugins, WindowsPlugin.kConfigKey);
  final Map<String, dynamic> context = <String, dynamic>{
    'plugins': windowsPlugins,
  };
  await _writeCppPluginRegistrant(project.windows.managedDirectory, context);
}

Future<void> _writeCppPluginRegistrant(Directory destination, Map<String, dynamic> templateContext) async {
  final String registryDirectory = destination.path;
  _renderTemplateToFile(
    _cppPluginRegistryHeaderTemplate,
    templateContext,
    fs.path.join(registryDirectory, 'generated_plugin_registrant.h'),
  );
  _renderTemplateToFile(
    _cppPluginRegistryImplementationTemplate,
    templateContext,
    fs.path.join(registryDirectory, 'generated_plugin_registrant.cc'),
  );
}

754 755 756 757 758 759 760 761 762
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;
  final String filePath = fs.path.join(registryDirectory, 'generated_plugin_registrant.dart');
  if (webPlugins.isEmpty) {
    final File file = fs.file(filePath);
763 764 765
    if (file.existsSync()) {
      file.deleteSync();
    }
766 767 768 769 770 771 772 773 774
  } else {
    _renderTemplateToFile(
      _dartPluginRegistryTemplate,
      context,
      filePath,
    );
  }
}

775 776 777
/// Rewrites the `.flutter-plugins` file of [project] based on the plugin
/// dependencies declared in `pubspec.yaml`.
///
778 779 780
/// If `checkProjects` is true, then plugins are only injected into directories
/// which already exist.
///
781
/// Assumes `pub get` has been executed since last change to `pubspec.yaml`.
782
void refreshPluginsList(FlutterProject project, {bool checkProjects = false}) {
783 784
  final List<Plugin> plugins = findPlugins(project);
  final bool changed = _writeFlutterPluginsList(project, plugins);
785
  if (changed) {
786 787 788 789 790 791 792
    if (!checkProjects || project.ios.existsSync()) {
      cocoaPods.invalidatePodInstallOutput(project.ios);
    }
    // TODO(stuartmorgan): Potentially add checkProjects once a decision has
    // made about how to handle macOS in existing projects.
    if (project.macos.existsSync()) {
      cocoaPods.invalidatePodInstallOutput(project.macos);
793 794
    }
  }
795 796
}

797
/// Injects plugins found in `pubspec.yaml` into the platform-specific projects.
798
///
799 800 801
/// If `checkProjects` is true, then plugins are only injected into directories
/// which already exist.
///
802
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
803
Future<void> injectPlugins(FlutterProject project, {bool checkProjects = false}) async {
804
  final List<Plugin> plugins = findPlugins(project);
805 806 807 808 809 810
  if ((checkProjects && project.android.existsSync()) || !checkProjects) {
    await _writeAndroidPluginRegistrant(project, plugins);
  }
  if ((checkProjects && project.ios.existsSync()) || !checkProjects) {
    await _writeIOSPluginRegistrant(project, plugins);
  }
811
  // TODO(stuartmorgan): Revisit the conditions here once the plans for handling
812 813
  // desktop in existing projects are in place. For now, ignore checkProjects
  // on desktop and always treat it as true.
814 815 816
  if (featureFlags.isLinuxEnabled && project.linux.existsSync()) {
    await _writeLinuxPluginRegistrant(project, plugins);
  }
817
  if (featureFlags.isMacOSEnabled && project.macos.existsSync()) {
818
    await _writeMacOSPluginRegistrant(project, plugins);
819 820 821
  }
  if (featureFlags.isWindowsEnabled && project.windows.existsSync()) {
    await _writeWindowsPluginRegistrant(project, plugins);
822 823
  }
  for (final XcodeBasedProject subproject in <XcodeBasedProject>[project.ios, project.macos]) {
824 825 826 827 828 829 830 831 832 833
    if (!project.isModule && (!checkProjects || subproject.existsSync())) {
      final CocoaPods cocoaPods = CocoaPods();
      if (plugins.isNotEmpty) {
        await cocoaPods.setupPodfile(subproject);
      }
      /// The user may have a custom maintained Podfile that they're running `pod install`
      /// on themselves.
      else if (subproject.podfile.existsSync() && subproject.podfileLock.existsSync()) {
        cocoaPods.addPodsDependencyToFlutterXcconfig(subproject);
      }
834
    }
835
  }
836 837 838
  if (featureFlags.isWebEnabled && project.web.existsSync()) {
    await _writeWebPluginRegistrant(project, plugins);
  }
839 840
}

841
/// Returns whether the specified Flutter [project] has any plugin dependencies.
842 843
///
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
844
bool hasPlugins(FlutterProject project) {
845
  return _readFileContent(project.flutterPluginsFile) != null;
846
}