• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:mustache/mustache.dart' as mustache;
8import 'package:yaml/yaml.dart';
9
10import 'base/file_system.dart';
11import 'dart/package_map.dart';
12import 'features.dart';
13import 'globals.dart';
14import 'macos/cocoapods.dart';
15import 'project.dart';
16
17void _renderTemplateToFile(String template, dynamic context, String filePath) {
18  final String renderedTemplate =
19     mustache.Template(template).renderString(context);
20  final File file = fs.file(filePath);
21  file.createSync(recursive: true);
22  file.writeAsStringSync(renderedTemplate);
23}
24
25class Plugin {
26  Plugin({
27    this.name,
28    this.path,
29    this.androidPackage,
30    this.iosPrefix,
31    this.macosPrefix,
32    this.pluginClass,
33  });
34
35  factory Plugin.fromYaml(String name, String path, dynamic pluginYaml) {
36    String androidPackage;
37    String iosPrefix;
38    String macosPrefix;
39    String pluginClass;
40    if (pluginYaml != null) {
41      androidPackage = pluginYaml['androidPackage'];
42      iosPrefix = pluginYaml['iosPrefix'] ?? '';
43      // TODO(stuartmorgan): Add |?? ''| here as well once this isn't used as
44      // an indicator of macOS support, see https://github.com/flutter/flutter/issues/33597
45      macosPrefix = pluginYaml['macosPrefix'];
46      pluginClass = pluginYaml['pluginClass'];
47    }
48    return Plugin(
49      name: name,
50      path: path,
51      androidPackage: androidPackage,
52      iosPrefix: iosPrefix,
53      macosPrefix: macosPrefix,
54      pluginClass: pluginClass,
55    );
56  }
57
58  final String name;
59  final String path;
60  final String androidPackage;
61  final String iosPrefix;
62  final String macosPrefix;
63  final String pluginClass;
64}
65
66Plugin _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  final dynamic pubspec = loadYaml(fs.file(pubspecPath).readAsStringSync());
71  if (pubspec == null)
72    return null;
73  final dynamic flutterConfig = pubspec['flutter'];
74  if (flutterConfig == null || !flutterConfig.containsKey('plugin'))
75    return null;
76  final String packageRootPath = fs.path.fromUri(packageRoot);
77  printTrace('Found plugin $name at $packageRootPath');
78  return Plugin.fromYaml(name, packageRootPath, flutterConfig['plugin']);
79}
80
81List<Plugin> findPlugins(FlutterProject project) {
82  final List<Plugin> plugins = <Plugin>[];
83  Map<String, Uri> packages;
84  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  packages.forEach((String name, Uri uri) {
92    final Uri packageRoot = uri.resolve('..');
93    final Plugin plugin = _pluginFromPubspec(name, packageRoot);
94    if (plugin != null)
95      plugins.add(plugin);
96  });
97  return plugins;
98}
99
100/// Returns true if .flutter-plugins has changed, otherwise returns false.
101bool _writeFlutterPluginsList(FlutterProject project, List<Plugin> plugins) {
102  final File pluginsFile = project.flutterPluginsFile;
103  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  return oldContents != newContents;
115}
116
117/// Returns the contents of the `.flutter-plugins` file in [project], or
118/// null if that file does not exist.
119String _readFlutterPluginsList(FlutterProject project) {
120  return project.flutterPluginsFile.existsSync()
121      ? project.flutterPluginsFile.readAsStringSync()
122      : null;
123}
124
125const String _androidPluginRegistryTemplate = '''package io.flutter.plugins;
126
127import io.flutter.plugin.common.PluginRegistry;
128{{#plugins}}
129import {{package}}.{{class}};
130{{/plugins}}
131
132/**
133 * Generated file. Do not edit.
134 */
135public final class GeneratedPluginRegistrant {
136  public static void registerWith(PluginRegistry registry) {
137    if (alreadyRegisteredWith(registry)) {
138      return;
139    }
140{{#plugins}}
141    {{class}}.registerWith(registry.registrarFor("{{package}}.{{class}}"));
142{{/plugins}}
143  }
144
145  private static boolean alreadyRegisteredWith(PluginRegistry registry) {
146    final String key = GeneratedPluginRegistrant.class.getCanonicalName();
147    if (registry.hasPlugin(key)) {
148      return true;
149    }
150    registry.registrarFor(key);
151    return false;
152  }
153}
154''';
155
156Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
157  final List<Map<String, dynamic>> androidPlugins = plugins
158      .where((Plugin p) => p.androidPackage != null && p.pluginClass != null)
159      .map<Map<String, dynamic>>((Plugin p) => <String, dynamic>{
160          'name': p.name,
161          'package': p.androidPackage,
162          'class': p.pluginClass,
163      })
164      .toList();
165  final Map<String, dynamic> context = <String, dynamic>{
166    'plugins': androidPlugins,
167  };
168
169  final String javaSourcePath = fs.path.join(
170    project.android.pluginRegistrantHost.path,
171    'src',
172    'main',
173    'java',
174  );
175  final String registryPath = fs.path.join(
176    javaSourcePath,
177    'io',
178    'flutter',
179    'plugins',
180    'GeneratedPluginRegistrant.java',
181  );
182  _renderTemplateToFile(_androidPluginRegistryTemplate, context, registryPath);
183}
184
185const String _objcPluginRegistryHeaderTemplate = '''//
186//  Generated file. Do not edit.
187//
188
189#ifndef GeneratedPluginRegistrant_h
190#define GeneratedPluginRegistrant_h
191
192#import <{{framework}}/{{framework}}.h>
193
194@interface GeneratedPluginRegistrant : NSObject
195+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
196@end
197
198#endif /* GeneratedPluginRegistrant_h */
199''';
200
201const String _objcPluginRegistryImplementationTemplate = '''//
202//  Generated file. Do not edit.
203//
204
205#import "GeneratedPluginRegistrant.h"
206{{#plugins}}
207#import <{{name}}/{{class}}.h>
208{{/plugins}}
209
210@implementation GeneratedPluginRegistrant
211
212+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
213{{#plugins}}
214  [{{prefix}}{{class}} registerWithRegistrar:[registry registrarForPlugin:@"{{prefix}}{{class}}"]];
215{{/plugins}}
216}
217
218@end
219''';
220
221const String _swiftPluginRegistryTemplate = '''//
222//  Generated file. Do not edit.
223//
224import Foundation
225import {{framework}}
226
227{{#plugins}}
228import {{name}}
229{{/plugins}}
230
231func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
232  {{#plugins}}
233  {{class}}.register(with: registry.registrar(forPlugin: "{{class}}"))
234{{/plugins}}
235}
236''';
237
238const String _pluginRegistrantPodspecTemplate = '''
239#
240# Generated file, do not edit.
241#
242
243Pod::Spec.new do |s|
244  s.name             = 'FlutterPluginRegistrant'
245  s.version          = '0.0.1'
246  s.summary          = 'Registers plugins with your flutter app'
247  s.description      = <<-DESC
248Depends on all your plugins, and provides a function to register them.
249                       DESC
250  s.homepage         = 'https://flutter.dev'
251  s.license          = { :type => 'BSD' }
252  s.author           = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
253  s.{{os}}.deployment_target = '{{deploymentTarget}}'
254  s.source_files =  "Classes", "Classes/**/*.{h,m}"
255  s.source           = { :path => '.' }
256  s.public_header_files = './Classes/**/*.h'
257  s.dependency '{{framework}}'
258  {{#plugins}}
259  s.dependency '{{name}}'
260  {{/plugins}}
261end
262''';
263
264Future<void> _writeIOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
265  final List<Map<String, dynamic>> iosPlugins = plugins
266      .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    'os': 'ios',
274    'deploymentTarget': '8.0',
275    'framework': 'Flutter',
276    'plugins': iosPlugins,
277  };
278  final String registryDirectory = project.ios.pluginRegistrantHost.path;
279  if (project.isModule) {
280    final String registryClassesDirectory = fs.path.join(registryDirectory, 'Classes');
281    _renderTemplateToFile(
282      _pluginRegistrantPodspecTemplate,
283      context,
284      fs.path.join(registryDirectory, 'FlutterPluginRegistrant.podspec'),
285    );
286    _renderTemplateToFile(
287      _objcPluginRegistryHeaderTemplate,
288      context,
289      fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.h'),
290    );
291    _renderTemplateToFile(
292      _objcPluginRegistryImplementationTemplate,
293      context,
294      fs.path.join(registryClassesDirectory, 'GeneratedPluginRegistrant.m'),
295    );
296  } else {
297    _renderTemplateToFile(
298      _objcPluginRegistryHeaderTemplate,
299      context,
300      fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.h'),
301    );
302    _renderTemplateToFile(
303      _objcPluginRegistryImplementationTemplate,
304      context,
305      fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.m'),
306    );
307  }
308}
309
310Future<void> _writeMacOSPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
311  // TODO(stuartmorgan): Replace macosPrefix check with formal metadata check,
312  // see https://github.com/flutter/flutter/issues/33597.
313  final List<Map<String, dynamic>> macosPlugins = plugins
314      .where((Plugin p) => p.pluginClass != null && p.macosPrefix != null)
315      .map<Map<String, dynamic>>((Plugin p) => <String, dynamic>{
316    'name': p.name,
317    'class': p.pluginClass,
318  }).toList();
319  final Map<String, dynamic> context = <String, dynamic>{
320    'os': 'macos',
321    'framework': 'FlutterMacOS',
322    'plugins': macosPlugins,
323  };
324  final String registryDirectory = project.macos.managedDirectory.path;
325  _renderTemplateToFile(
326    _swiftPluginRegistryTemplate,
327    context,
328    fs.path.join(registryDirectory, 'GeneratedPluginRegistrant.swift'),
329  );
330}
331
332/// Rewrites the `.flutter-plugins` file of [project] based on the plugin
333/// dependencies declared in `pubspec.yaml`.
334///
335/// If `checkProjects` is true, then plugins are only injected into directories
336/// which already exist.
337///
338/// Assumes `pub get` has been executed since last change to `pubspec.yaml`.
339void refreshPluginsList(FlutterProject project, {bool checkProjects = false}) {
340  final List<Plugin> plugins = findPlugins(project);
341  final bool changed = _writeFlutterPluginsList(project, plugins);
342  if (changed) {
343    if (checkProjects && !project.ios.existsSync()) {
344      return;
345    }
346    cocoaPods.invalidatePodInstallOutput(project.ios);
347  }
348}
349
350/// Injects plugins found in `pubspec.yaml` into the platform-specific projects.
351///
352/// If `checkProjects` is true, then plugins are only injected into directories
353/// which already exist.
354///
355/// Assumes [refreshPluginsList] has been called since last change to `pubspec.yaml`.
356Future<void> injectPlugins(FlutterProject project, {bool checkProjects = false}) async {
357  final List<Plugin> plugins = findPlugins(project);
358  if ((checkProjects && project.android.existsSync()) || !checkProjects) {
359    await _writeAndroidPluginRegistrant(project, plugins);
360  }
361  if ((checkProjects && project.ios.existsSync()) || !checkProjects) {
362    await _writeIOSPluginRegistrant(project, plugins);
363  }
364  // TODO(stuartmorgan): Revisit the condition here once the plans for handling
365  // desktop in existing projects are in place. For now, ignore checkProjects
366  // on desktop and always treat it as true.
367  if (featureFlags.isMacOSEnabled && project.macos.existsSync()) {
368    await _writeMacOSPluginRegistrant(project, plugins);
369  }
370  for (final XcodeBasedProject subproject in <XcodeBasedProject>[project.ios, project.macos]) {
371  if (!project.isModule && (!checkProjects || subproject.existsSync())) {
372    final CocoaPods cocoaPods = CocoaPods();
373    if (plugins.isNotEmpty) {
374      cocoaPods.setupPodfile(subproject);
375    }
376    /// The user may have a custom maintained Podfile that they're running `pod install`
377    /// on themselves.
378    else if (subproject.podfile.existsSync() && subproject.podfileLock.existsSync()) {
379      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`.
388bool hasPlugins(FlutterProject project) {
389  return _readFlutterPluginsList(project) != null;
390}
391