plugins.dart 12.4 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// 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:mustache/mustache.dart' as mustache;
8 9 10 11
import 'package:yaml/yaml.dart';

import 'base/file_system.dart';
import 'dart/package_map.dart';
12
import 'features.dart';
13
import 'globals.dart';
14
import 'macos/cocoapods.dart';
15
import 'project.dart';
16

17 18
void _renderTemplateToFile(String template, dynamic context, String filePath) {
  final String renderedTemplate =
19
     mustache.Template(template).renderString(context);
20 21 22 23 24
  final File file = fs.file(filePath);
  file.createSync(recursive: true);
  file.writeAsStringSync(renderedTemplate);
}

25
class Plugin {
26 27 28 29 30
  Plugin({
    this.name,
    this.path,
    this.androidPackage,
    this.iosPrefix,
31
    this.macosPrefix,
32 33
    this.pluginClass,
  });
34 35 36

  factory Plugin.fromYaml(String name, String path, dynamic pluginYaml) {
    String androidPackage;
37
    String iosPrefix;
38
    String macosPrefix;
39 40 41
    String pluginClass;
    if (pluginYaml != null) {
      androidPackage = pluginYaml['androidPackage'];
42
      iosPrefix = pluginYaml['iosPrefix'] ?? '';
43 44 45
      // TODO(stuartmorgan): Add |?? ''| here as well once this isn't used as
      // an indicator of macOS support, see https://github.com/flutter/flutter/issues/33597
      macosPrefix = pluginYaml['macosPrefix'];
46 47
      pluginClass = pluginYaml['pluginClass'];
    }
48
    return Plugin(
49 50 51 52
      name: name,
      path: path,
      androidPackage: androidPackage,
      iosPrefix: iosPrefix,
53
      macosPrefix: macosPrefix,
54 55
      pluginClass: pluginClass,
    );
56
  }
57 58 59 60 61

  final String name;
  final String path;
  final String androidPackage;
  final String iosPrefix;
62
  final String macosPrefix;
63
  final String pluginClass;
64 65 66
}

Plugin _pluginFromPubspec(String name, Uri packageRoot) {
67
  final String pubspecPath = fs.path.fromUri(packageRoot.resolve('pubspec.yaml'));
68
  if (!fs.isFileSync(pubspecPath))
69
    return null;
70 71 72 73 74 75
  final dynamic pubspec = loadYaml(fs.file(pubspecPath).readAsStringSync());
  if (pubspec == null)
    return null;
  final dynamic flutterConfig = pubspec['flutter'];
  if (flutterConfig == null || !flutterConfig.containsKey('plugin'))
    return null;
76 77
  final String packageRootPath = fs.path.fromUri(packageRoot);
  printTrace('Found plugin $name at $packageRootPath');
78
  return Plugin.fromYaml(name, packageRootPath, flutterConfig['plugin']);
79 80
}

81
List<Plugin> findPlugins(FlutterProject project) {
82
  final List<Plugin> plugins = <Plugin>[];
83 84
  Map<String, Uri> packages;
  try {
85
    final String packagesFile = fs.path.join(project.directory.path, PackageMap.globalPackagesPath);
86
    packages = PackageMap(packagesFile).map;
87
  } on FormatException catch (e) {
88
    printTrace('Invalid .packages file: $e');
89
    return plugins;
90 91 92
  }
  packages.forEach((String name, Uri uri) {
    final Uri packageRoot = uri.resolve('..');
93 94 95
    final Plugin plugin = _pluginFromPubspec(name, packageRoot);
    if (plugin != null)
      plugins.add(plugin);
96
  });
97
  return plugins;
98 99
}

100
/// Returns true if .flutter-plugins has changed, otherwise returns false.
101 102 103
bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) {
  final File pluginsFile = project.flutterPluginsFile;
  final String oldContents = _readFlutterPluginsList(project);
104
  final String pluginManifest =
105
      plugins.map<String>((Plugin p) => '${p.name}=${escapePath(p.path)}').join('\n');
106
  if (pluginManifest.isNotEmpty) {
107
    pluginsFile.writeAsStringSync('$pluginManifest\n', flush: true);
108
  } else {
109
    if (pluginsFile.existsSync()) {
110
      pluginsFile.deleteSync();
111
    }
112
  }
113
  final String newContents = _readFlutterPluginsList(project);
114 115 116
  return oldContents != newContents;
}

117
/// Returns the contents of the `.flutter-plugins` file in [project], or
118
/// null if that file does not exist.
119 120 121 122
String _readFlutterPluginsList(FlutterProject project) {
  return project.flutterPluginsFile.existsSync()
      ? project.flutterPluginsFile.readAsStringSync()
      : null;
123
}
124 125 126

const String _androidPluginRegistryTemplate = '''package io.flutter.plugins;

127
import io.flutter.plugin.common.PluginRegistry;
128
{{#plugins}}
129 130 131 132 133 134
import {{package}}.{{class}};
{{/plugins}}

/**
 * Generated file. Do not edit.
 */
135 136
public final class GeneratedPluginRegistrant {
  public static void registerWith(PluginRegistry registry) {
137 138 139
    if (alreadyRegisteredWith(registry)) {
      return;
    }
140
{{#plugins}}
141
    {{class}}.registerWith(registry.registrarFor("{{package}}.{{class}}"));
142
{{/plugins}}
143
  }
144 145 146 147 148 149 150 151 152

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

156
Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
157 158
  final List<Map<String, dynamic>> androidPlugins = plugins
      .where((Plugin p) => p.androidPackage != null && p.pluginClass != null)
159
      .map<Map<String, dynamic>>((Plugin p) => <String, dynamic>{
160 161 162 163 164 165 166 167 168
          'name': p.name,
          'package': p.androidPackage,
          'class': p.pluginClass,
      })
      .toList();
  final Map<String, dynamic> context = <String, dynamic>{
    'plugins': androidPlugins,
  };

169
  final String javaSourcePath = fs.path.join(
170
    project.android.pluginRegistrantHost.path,
171 172 173 174 175 176 177 178 179 180 181
    'src',
    'main',
    'java',
  );
  final String registryPath = fs.path.join(
    javaSourcePath,
    'io',
    'flutter',
    'plugins',
    'GeneratedPluginRegistrant.java',
  );
182
  _renderTemplateToFile(_androidPluginRegistryTemplate, context, registryPath);
183 184
}

185
const String _objcPluginRegistryHeaderTemplate = '''//
186 187 188
//  Generated file. Do not edit.
//

189 190
#ifndef GeneratedPluginRegistrant_h
#define GeneratedPluginRegistrant_h
191

192
#import <{{framework}}/{{framework}}.h>
193

194 195
@interface GeneratedPluginRegistrant : NSObject
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
196 197
@end

198
#endif /* GeneratedPluginRegistrant_h */
199 200
''';

201
const String _objcPluginRegistryImplementationTemplate = '''//
202 203 204
//  Generated file. Do not edit.
//

205
#import "GeneratedPluginRegistrant.h"
206 207 208
{{#plugins}}
#import <{{name}}/{{class}}.h>
{{/plugins}}
209

210
@implementation GeneratedPluginRegistrant
211

212
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
213
{{#plugins}}
214
  [{{prefix}}{{class}} registerWithRegistrar:[registry registrarForPlugin:@"{{prefix}}{{class}}"]];
215 216 217 218 219 220
{{/plugins}}
}

@end
''';

221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
const String _swiftPluginRegistryTemplate = '''//
//  Generated file. Do not edit.
//
import Foundation
import {{framework}}

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

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

238
const String _pluginRegistrantPodspecTemplate = '''
239 240 241 242 243 244 245 246 247 248 249
#
# 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
250
  s.homepage         = 'https://flutter.dev'
251 252
  s.license          = { :type => 'BSD' }
  s.author           = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
253
  s.{{os}}.deployment_target = '{{deploymentTarget}}'
254 255 256
  s.source_files =  "Classes", "Classes/**/*.{h,m}"
  s.source           = { :path => '.' }
  s.public_header_files = './Classes/**/*.h'
257
  s.dependency '{{framework}}'
258 259 260 261 262 263
  {{#plugins}}
  s.dependency '{{name}}'
  {{/plugins}}
end
''';

264
Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
265 266
  final List<Map<String, dynamic>> iosPlugins = plugins
      .where((Plugin p) => p.pluginClass != null)
267
      .map<Map<String, dynamic>>((Plugin p) => <String, dynamic>{
268
    'name': p.name,
269
    'prefix': p.iosPrefix,
270
    'class': p.pluginClass,
271
  }).toList();
272
  final Map<String, dynamic> context = <String, dynamic>{
273 274 275
    'os': 'ios',
    'deploymentTarget': '8.0',
    'framework': 'Flutter',
276 277
    'plugins': iosPlugins,
  };
278
  final String registryDirectory = project.ios.pluginRegistrantHost.path;
279
  if (project.isModule) {
280 281
    final String registryClassesDirectory = fs.path.join(registryDirectory, 'Classes');
    _renderTemplateToFile(
282
      _pluginRegistrantPodspecTemplate,
283
      context,
284 285 286
      fs.path.join(registryDirectory, 'FlutterPluginRegistrant.podspec'),
    );
    _renderTemplateToFile(
287 288
      _objcPluginRegistryHeaderTemplate,
      context,
289 290 291
      fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.h'),
    );
    _renderTemplateToFile(
292 293
      _objcPluginRegistryImplementationTemplate,
      context,
294 295 296 297
      fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.m'),
    );
  } else {
    _renderTemplateToFile(
298 299
      _objcPluginRegistryHeaderTemplate,
      context,
300
      fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.h'),
301 302
    );
    _renderTemplateToFile(
303 304
      _objcPluginRegistryImplementationTemplate,
      context,
305
      fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.m'),
306 307
    );
  }
308 309
}

310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
  // TODO(stuartmorgan): Replace macosPrefix check with formal metadata check,
  // see https://github.com/flutter/flutter/issues/33597.
  final List<Map<String, dynamic>> macosPlugins = plugins
      .where((Plugin p) => p.pluginClass != null && p.macosPrefix != null)
      .map<Map<String, dynamic>>((Plugin p) => <String, dynamic>{
    'name': p.name,
    'class': p.pluginClass,
  }).toList();
  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'),
  );
}

332 333 334
/// Rewrites the `.flutter-plugins` file of [project] based on the plugin
/// dependencies declared in `pubspec.yaml`.
///
335 336 337
/// If `checkProjects` is true, then plugins are only injected into directories
/// which already exist.
///
338
/// Assumes `pub get` has been executed since last change to `pubspec.yaml`.
339
void refreshPluginsList(FlutterProject project, {bool checkProjects = false}) {
340 341
  final List<Plugin> plugins = findPlugins(project);
  final bool changed = _writeFlutterPluginsList(project, plugins);
342 343 344 345
  if (changed) {
    if (checkProjects && !project.ios.existsSync()) {
      return;
    }
346
    cocoaPods.invalidatePodInstallOutput(project.ios);
347
  }
348 349
}

350
/// Injects plugins found in `pubspec.yaml` into the platform-specific projects.
351
///
352 353 354
/// If `checkProjects` is true, then plugins are only injected into directories
/// which already exist.
///
355
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
356
Future<void> injectPlugins(FlutterProject project, {bool checkProjects = false}) async {
357
  final List<Plugin> plugins = findPlugins(project);
358 359 360 361 362 363
  if ((checkProjects && project.android.existsSync()) || !checkProjects) {
    await _writeAndroidPluginRegistrant(project, plugins);
  }
  if ((checkProjects && project.ios.existsSync()) || !checkProjects) {
    await _writeIOSPluginRegistrant(project, plugins);
  }
364 365 366
  // TODO(stuartmorgan): Revisit the condition here once the plans for handling
  // desktop in existing projects are in place. For now, ignore checkProjects
  // on desktop and always treat it as true.
367
  if (featureFlags.isMacOSEnabled && project.macos.existsSync()) {
368 369 370 371
    await _writeMacOSPluginRegistrant(project, plugins);
  }
  for (final XcodeBasedProject subproject in <XcodeBasedProject>[project.ios, project.macos]) {
  if (!project.isModule && (!checkProjects || subproject.existsSync())) {
372
    final CocoaPods cocoaPods = CocoaPods();
373
    if (plugins.isNotEmpty) {
374
      cocoaPods.setupPodfile(subproject);
375 376 377
    }
    /// The user may have a custom maintained Podfile that they're running `pod install`
    /// on themselves.
378 379
    else if (subproject.podfile.existsSync() && subproject.podfileLock.existsSync()) {
      cocoaPods.addPodsDependencyToFlutterXcconfig(subproject);
380
    }
381
  }
382
  }
383 384
}

385
/// Returns whether the specified Flutter [project] has any plugin dependencies.
386 387
///
/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
388 389
bool hasPlugins(FlutterProject project) {
  return _readFlutterPluginsList(project) != null;
390
}