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