Automatically wire dependencies for native plugins (#8891)

Go through all packages brought in by pub, and write the name and path of every one that is a flutter plugin into .flutter-plugins.

In android/settings.gradle and ios/Podfile, read in .flutter-plugins, if that file exists. The Android / iOS code from the plugins is automatically added as dependencies of the native code of the app.
diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle
index 7e16be7e..29cc75a 100644
--- a/packages/flutter_tools/gradle/flutter.gradle
+++ b/packages/flutter_tools/gradle/flutter.gradle
@@ -34,13 +34,22 @@
     private String localEngineSrcPath
     private Properties localProperties
 
+    private File flutterJar
+    private File debugFlutterJar
+    private File profileFlutterJar
+    private File releaseFlutterJar
+
+    private Properties readPropertiesIfExist(File propertiesFile) {
+        Properties result = new Properties()
+        if (propertiesFile.exists()) {
+            propertiesFile.withInputStream { stream -> result.load(stream) }
+        }
+        return result
+    }
+
     private String resolveProperty(Project project, String name, String defaultValue) {
         if (localProperties == null) {
-            localProperties = new Properties()
-            def localPropertiesFile = project.rootProject.file("local.properties")
-            if (localPropertiesFile.exists()) {
-                localProperties.load(localPropertiesFile.newDataInputStream())
-            }
+            localProperties = readPropertiesIfExist(project.rootProject.file("local.properties"))
         }
         String result
         if (project.hasProperty(name)) {
@@ -82,7 +91,7 @@
             if (!engineOut.isDirectory()) {
                 throw new GradleException('localEngineOut must point to a local engine build')
             }
-            File flutterJar = Paths.get(engineOut.absolutePath, "flutter.jar").toFile()
+            flutterJar = Paths.get(engineOut.absolutePath, "flutter.jar").toFile()
             if (!flutterJar.isFile()) {
                 throw new GradleException('Local engine build does not contain flutter.jar')
             }
@@ -95,9 +104,9 @@
             }
         } else {
             Path baseEnginePath = Paths.get(flutterRoot.absolutePath, "bin", "cache", "artifacts", "engine")
-            File debugFlutterJar = baseEnginePath.resolve("android-arm").resolve("flutter.jar").toFile()
-            File profileFlutterJar = baseEnginePath.resolve("android-arm-profile").resolve("flutter.jar").toFile()
-            File releaseFlutterJar = baseEnginePath.resolve("android-arm-release").resolve("flutter.jar").toFile()
+            debugFlutterJar = baseEnginePath.resolve("android-arm").resolve("flutter.jar").toFile()
+            profileFlutterJar = baseEnginePath.resolve("android-arm-profile").resolve("flutter.jar").toFile()
+            releaseFlutterJar = baseEnginePath.resolve("android-arm-release").resolve("flutter.jar").toFile()
             if (!debugFlutterJar.isFile()) {
                 project.exec {
                     executable flutterExecutable.absolutePath
@@ -130,6 +139,32 @@
 
         project.extensions.create("flutter", FlutterExtension)
         project.afterEvaluate this.&addFlutterTask
+
+        File pluginsFile = new File(project.rootProject.projectDir.parentFile, '.flutter-plugins')
+        Properties plugins = readPropertiesIfExist(pluginsFile)
+
+        plugins.each { name, _ ->
+            def pluginProject = project.rootProject.findProject(":$name")
+            if (pluginProject != null) {
+                project.dependencies {
+                    compile pluginProject
+                }
+                pluginProject.afterEvaluate this.&addFlutterJarDependency
+            } else {
+                project.logger.error("Plugin project :$name not found. Please update settings.gradle.")
+            }
+        }
+    }
+
+    private void addFlutterJarDependency(Project project) {
+        project.dependencies {
+            if (flutterJar != null) {
+                provided project.files(flutterJar)
+            } else {
+                debugProvided project.files(debugFlutterJar)
+                releaseProvided project.files(releaseFlutterJar)
+            }
+        }
     }
 
     private void addFlutterTask(Project project) {
diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart
index 5e5984b..48e3e8d 100644
--- a/packages/flutter_tools/lib/src/android/gradle.dart
+++ b/packages/flutter_tools/lib/src/android/gradle.dart
@@ -15,6 +15,7 @@
 import '../build_info.dart';
 import '../cache.dart';
 import '../globals.dart';
+import '../plugins.dart';
 import 'android_sdk.dart';
 import 'android_studio.dart';
 
@@ -154,6 +155,8 @@
   settings.values['flutter.buildMode'] = buildModeName;
   settings.writeContents(localProperties);
 
+  writeFlutterPluginsList();
+
   final String gradle = ensureGradle();
 
   switch (flutterPluginVersion) {
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index e20e793..e26e0d7 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -19,6 +19,7 @@
 import '../doctor.dart';
 import '../flx.dart' as flx;
 import '../globals.dart';
+import '../plugins.dart';
 import '../services.dart';
 import 'xcodeproj.dart';
 
@@ -127,6 +128,7 @@
   // copied over to a location that is suitable for Xcodebuild to find them.
   final Directory appDirectory = fs.directory(app.appDirectory);
   await _addServicesToBundle(appDirectory);
+  writeFlutterPluginsList();
 
   _runPodInstall(appDirectory, flutterFrameworkDir(mode));
 
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
new file mode 100644
index 0000000..0df1721
--- /dev/null
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -0,0 +1,52 @@
+// 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.
+
+import 'package:yaml/yaml.dart';
+
+import 'base/file_system.dart';
+import 'dart/package_map.dart';
+import 'globals.dart';
+
+dynamic _loadYamlFile(String path) {
+  if (!fs.isFileSync(path))
+    return null;
+  final String manifestString = fs.file(path).readAsStringSync();
+  return loadYaml(manifestString);
+}
+
+String _generatePluginManifest() {
+  Map<String, Uri> packages;
+  try {
+    packages = new PackageMap(PackageMap.globalPackagesPath).map;
+  } on FormatException catch(e) {
+    printTrace('Invalid .packages file: $e');
+    return '';
+  }
+  final List<String> plugins = <String>[];
+  packages.forEach((String name, Uri uri) {
+    final Uri packageRoot = uri.resolve('..');
+    final dynamic packageConfig = _loadYamlFile(packageRoot.resolve('pubspec.yaml').path);
+    if (packageConfig != null) {
+      final dynamic flutterConfig = packageConfig['flutter'];
+      if (flutterConfig != null && flutterConfig.containsKey('plugin')) {
+        printTrace('Found plugin $name at ${packageRoot.path}');
+        plugins.add('$name=${packageRoot.path}');
+      }
+    }
+  });
+  return plugins.join('\n');
+}
+
+void writeFlutterPluginsList() {
+  final File pluginsProperties = fs.file('.flutter-plugins');
+
+  final String pluginManifest = _generatePluginManifest();
+  if (pluginManifest.isNotEmpty) {
+    pluginsProperties.writeAsStringSync('$pluginManifest\n');
+  } else {
+    if (pluginsProperties.existsSync()) {
+      pluginsProperties.deleteSync();
+    }
+  }
+}
diff --git a/packages/flutter_tools/templates/create/.gitignore.tmpl b/packages/flutter_tools/templates/create/.gitignore.tmpl
index 14c7d4c..eb15c3d 100644
--- a/packages/flutter_tools/templates/create/.gitignore.tmpl
+++ b/packages/flutter_tools/templates/create/.gitignore.tmpl
@@ -7,3 +7,4 @@
 ios/.generated/
 packages
 pubspec.lock
+.flutter-plugins
diff --git a/packages/flutter_tools/templates/create/android.tmpl/settings.gradle b/packages/flutter_tools/templates/create/android.tmpl/settings.gradle
index e7b4def..115da6c 100644
--- a/packages/flutter_tools/templates/create/android.tmpl/settings.gradle
+++ b/packages/flutter_tools/templates/create/android.tmpl/settings.gradle
@@ -1 +1,15 @@
 include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+    pluginsFile.withInputStream { stream -> plugins.load(stream) }
+}
+
+plugins.each { name, path ->
+    def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+    include ":$name"
+    project(":$name").projectDir = pluginDirectory
+}
diff --git a/packages/flutter_tools/templates/create/ios.tmpl/Podfile b/packages/flutter_tools/templates/create/ios.tmpl/Podfile
index b6f1c98..74b3de0 100644
--- a/packages/flutter_tools/templates/create/ios.tmpl/Podfile
+++ b/packages/flutter_tools/templates/create/ios.tmpl/Podfile
@@ -1,10 +1,38 @@
 # Uncomment this line to define a global platform for your project
 # platform :ios, '9.0'
 
+if ENV['FLUTTER_FRAMEWORK_DIR'] == nil
+  abort('Please set FLUTTER_FRAMEWORK_DIR to the directory containing Flutter.framework')
+end
+
 target 'Runner' do
-  # Uncomment this line if you're using Swift or would like to use dynamic frameworks
-  # use_frameworks!
+  use_frameworks!
 
   # Pods for Runner
 
+  # Flutter Pods
+  pod 'Flutter', :path => ENV['FLUTTER_FRAMEWORK_DIR']
+
+  if File.exists? '../.flutter-plugins'
+    flutter_root = File.expand_path('..')
+    File.foreach('../.flutter-plugins') { |line|
+      plugin = line.split(pattern='=')
+      if plugin.length == 2
+        name = plugin[0].strip()
+        path = plugin[1].strip()
+        resolved_path = File.expand_path("#{path}/ios", flutter_root)
+        pod name, :path => resolved_path
+      else
+        puts "Invalid plugin specification: #{line}"
+      end
+    }
+  end
+end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    target.build_configurations.each do |config|
+      config.build_settings['ENABLE_BITCODE'] = 'NO'
+    end
+  end
 end