Revert pod install skip revert (#13774)

* Revert "Revert "Enable developers to run pod from terminal directly and skip pod install if possible. (#13374)" (#13770)"

This reverts commit 0759043e47395b692bff7d6aa045c1ecb06627eb.

* some nits on cocoapods code

* put back the FLUTTER_FRAMEWORK_DIR env variable
diff --git a/packages/flutter_tools/lib/src/commands/inject_plugins.dart b/packages/flutter_tools/lib/src/commands/inject_plugins.dart
index bf53cb5..f1df432 100644
--- a/packages/flutter_tools/lib/src/commands/inject_plugins.dart
+++ b/packages/flutter_tools/lib/src/commands/inject_plugins.dart
@@ -24,7 +24,7 @@
 
   @override
   Future<Null> runCommand() async {
-    final bool result = injectPlugins();
+    final bool result = injectPlugins().hasPlugin;
     if (result) {
       printStatus('GeneratedPluginRegistrants successfully written.');
     } else {
diff --git a/packages/flutter_tools/lib/src/ios/cocoapods.dart b/packages/flutter_tools/lib/src/ios/cocoapods.dart
index 78fd425..9ebe95a 100644
--- a/packages/flutter_tools/lib/src/ios/cocoapods.dart
+++ b/packages/flutter_tools/lib/src/ios/cocoapods.dart
@@ -57,18 +57,21 @@
 
   Future<Null> processPods({
     @required Directory appIosDir,
+    // For backward compatibility with previously created Podfile only.
     @required String iosEngineDir,
     bool isSwift: false,
+    bool pluginOrFlutterPodChanged: true,
   }) async {
     if (await _checkPodCondition()) {
       if (!fs.file(fs.path.join(appIosDir.path, 'Podfile')).existsSync()) {
         await _createPodfile(appIosDir, isSwift);
       } // TODO(xster): Add more logic for handling merge conflicts.
-
-      await _runPodInstall(appIosDir, iosEngineDir);
+      if (_shouldRunPodInstall(appIosDir.path, pluginOrFlutterPodChanged))
+        await _runPodInstall(appIosDir, iosEngineDir);
     }
   }
 
+  /// Make sure the CocoaPods tools are in the right states.
   Future<bool> _checkPodCondition() async {
     if (!await isCocoaPodsInstalledAndMeetsVersionCheck) {
       final String minimumVersion = cocoaPodsMinimumVersion;
@@ -108,12 +111,36 @@
     podfileTemplate.copySync(fs.path.join(bundle.path, 'Podfile'));
   }
 
+  // Check if you need to run pod install.
+  // The pod install will run if any of below is true.
+  // 1. Any plugins changed (add/update/delete)
+  // 2. The flutter.framework has changed (debug/release/profile)
+  // 3. The podfile.lock doesn't exists
+  // 4. The Pods/manifest.lock doesn't exists
+  // 5. The podfile.lock doesn't match Pods/manifest.lock.
+  bool _shouldRunPodInstall(String appDir, bool pluginOrFlutterPodChanged) {
+    if (pluginOrFlutterPodChanged)
+      return true;
+    // Check if podfile.lock and Pods/Manifest.lock exists and matches.
+    final File podfileLockFile = fs.file(fs.path.join(appDir, 'Podfile.lock'));
+    final File manifestLockFile =
+        fs.file(fs.path.join(appDir, 'Pods', 'Manifest.lock'));
+    if (!podfileLockFile.existsSync()
+        || !manifestLockFile.existsSync()
+        || podfileLockFile.readAsStringSync() !=
+            manifestLockFile.readAsStringSync()) {
+      return true;
+    }
+    return false;
+  }
+
   Future<Null> _runPodInstall(Directory bundle, String engineDirectory) async {
     final Status status = logger.startProgress('Running pod install...', expectSlowOperation: true);
     final ProcessResult result = await processManager.run(
       <String>['pod', 'install', '--verbose'],
       workingDirectory: bundle.path,
       environment: <String, String>{
+        // For backward compatibility with previously created Podfile only.
         'FLUTTER_FRAMEWORK_DIR': engineDirectory,
         // See https://github.com/flutter/flutter/issues/10873.
         // CocoaPods analytics adds a lot of latency.
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart
index e3ead3b..2ca8109 100644
--- a/packages/flutter_tools/lib/src/ios/mac.dart
+++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -254,14 +254,9 @@
   // copied over to a location that is suitable for Xcodebuild to find them.
   final Directory appDirectory = fs.directory(app.appDirectory);
   await _addServicesToBundle(appDirectory);
-  final bool hasFlutterPlugins = injectPlugins();
-
-  if (hasFlutterPlugins)
-    await cocoaPods.processPods(
-      appIosDir: appDirectory,
-      iosEngineDir: flutterFrameworkDir(buildInfo.mode),
-      isSwift: app.isSwift,
-    );
+  final InjectPluginsResult injectPluginsResult = injectPlugins();
+  final bool hasFlutterPlugins = injectPluginsResult.hasPlugin;
+  final String previousGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory);
 
   updateXcodeGeneratedProperties(
     projectPath: fs.currentDirectory.path,
@@ -271,6 +266,17 @@
     previewDart2: buildInfo.previewDart2,
   );
 
+  if (hasFlutterPlugins) {
+    final String currentGeneratedXcconfig = readGeneratedXcconfig(app.appDirectory);
+    await cocoaPods.processPods(
+        appIosDir: appDirectory,
+        iosEngineDir: flutterFrameworkDir(buildInfo.mode),
+        isSwift: app.isSwift,
+        pluginOrFlutterPodChanged: (injectPluginsResult.hasChanged
+            || previousGeneratedXcconfig != currentGeneratedXcconfig),
+    );
+  }
+
   final List<String> commands = <String>[
     '/usr/bin/env',
     'xcrun',
@@ -355,7 +361,17 @@
   }
 }
 
-Future<Null> diagnoseXcodeBuildFailure(XcodeBuildResult result, BuildableIOSApp app) async {
+String readGeneratedXcconfig(String appPath) {
+  final String generatedXcconfigPath =
+      fs.path.join(fs.currentDirectory.path, appPath, 'Flutter','Generated.xcconfig');
+  final File generatedXcconfigFile = fs.file(generatedXcconfigPath);
+  if (!generatedXcconfigFile.existsSync())
+    return null;
+  return generatedXcconfigFile.readAsStringSync();
+}
+
+Future<Null> diagnoseXcodeBuildFailure(
+    XcodeBuildResult result, BuildableIOSApp app) async {
   if (result.xcodeBuildExecution != null &&
       result.xcodeBuildExecution.buildForPhysicalDevice &&
       result.stdout?.contains('BCEROR') == true &&
@@ -369,7 +385,8 @@
       // Make sure the user has specified one of:
       // DEVELOPMENT_TEAM (automatic signing)
       // PROVISIONING_PROFILE (manual signing)
-      !(app.buildSettings?.containsKey('DEVELOPMENT_TEAM')) == true || app.buildSettings?.containsKey('PROVISIONING_PROFILE') == true) {
+      !(app.buildSettings?.containsKey('DEVELOPMENT_TEAM')) == true
+          || app.buildSettings?.containsKey('PROVISIONING_PROFILE') == true) {
     printError(noDevelopmentTeamInstruction, emphasis: true);
     return;
   }
diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart
index 19d7bc0..60f7c5c 100644
--- a/packages/flutter_tools/lib/src/plugins.dart
+++ b/packages/flutter_tools/lib/src/plugins.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:meta/meta.dart';
 import 'package:mustache/mustache.dart' as mustache;
 import 'package:yaml/yaml.dart';
 
@@ -77,10 +78,13 @@
   return plugins;
 }
 
-void _writeFlutterPluginsList(String directory, List<Plugin> plugins) {
+/// Returns true if .flutter-plugins has changed, otherwise returns false.
+bool _writeFlutterPluginsList(String directory, List<Plugin> plugins) {
   final File pluginsProperties = fs.file(fs.path.join(directory, '.flutter-plugins'));
+  final String previousFlutterPlugins =
+      (pluginsProperties.existsSync() ? pluginsProperties.readAsStringSync() : null);
   final String pluginManifest =
-    plugins.map((Plugin p) => '${p.name}=${escapePath(p.path)}').join('\n');
+      plugins.map((Plugin p) => '${p.name}=${escapePath(p.path)}').join('\n');
   if (pluginManifest.isNotEmpty) {
     pluginsProperties.writeAsStringSync('$pluginManifest\n');
   } else {
@@ -88,6 +92,9 @@
       pluginsProperties.deleteSync();
     }
   }
+  final String currentFlutterPlugins =
+      (pluginsProperties.existsSync() ? pluginsProperties.readAsStringSync() : null);
+  return currentFlutterPlugins != previousFlutterPlugins;
 }
 
 const String _androidPluginRegistryTemplate = '''package io.flutter.plugins;
@@ -206,17 +213,28 @@
 
 }
 
+class InjectPluginsResult{
+  InjectPluginsResult({
+    @required this.hasPlugin,
+    @required this.hasChanged,
+  });
+  /// True if any flutter plugin exists, otherwise false.
+  final bool hasPlugin;
+  /// True if plugins have changed since last build.
+  final bool hasChanged;
+}
+
 /// Finds Flutter plugins in the pubspec.yaml, creates platform injection
 /// registries classes and add them to the build dependencies.
 ///
-/// Returns whether any Flutter plugins are added.
-bool injectPlugins({String directory}) {
+/// Returns whether any Flutter plugins are added and whether they changed.
+InjectPluginsResult injectPlugins({String directory}) {
   directory ??= fs.currentDirectory.path;
   final List<Plugin> plugins = _findPlugins(directory);
-  _writeFlutterPluginsList(directory, plugins);
+  final bool hasPluginsChanged = _writeFlutterPluginsList(directory, plugins);
   if (fs.isDirectorySync(fs.path.join(directory, 'android')))
     _writeAndroidPluginRegistry(directory, plugins);
   if (fs.isDirectorySync(fs.path.join(directory, 'ios')))
     _writeIOSPluginRegistry(directory, plugins);
-  return plugins.isNotEmpty;
+  return new InjectPluginsResult(hasPlugin: plugins.isNotEmpty, hasChanged: hasPluginsChanged);
 }
diff --git a/packages/flutter_tools/templates/cocoapods/Podfile-objc b/packages/flutter_tools/templates/cocoapods/Podfile-objc
index 90b5f65..3bc469f 100644
--- a/packages/flutter_tools/templates/cocoapods/Podfile-objc
+++ b/packages/flutter_tools/templates/cocoapods/Podfile-objc
@@ -1,30 +1,44 @@
 # 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
-  # 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='=')
+def parse_KV_file(file,seperator='=')
+  file_abs_path = File.expand_path(file)
+  if !File.exists? file_abs_path
+    return [];
+  end
+  pods_ary = []
+  File.foreach(file_abs_path) { |line|
+      plugin = line.split(pattern=seperator)
       if plugin.length == 2
-        name = plugin[0].strip()
+        podname = plugin[0].strip()
         path = plugin[1].strip()
-        resolved_path = File.expand_path("#{path}/ios", flutter_root)
-        pod name, :path => resolved_path
+        podpath = File.expand_path("#{path}", file_abs_path)
+        pods_ary.push({:name => podname,:path=>podpath});
       else
         puts "Invalid plugin specification: #{line}"
       end
-    }
+  }
+  return pods_ary
+end
+
+target 'Runner' do
+  use_frameworks!
+  # Flutter Pods
+  generated_xcode_build_settings = parse_KV_file("./Flutter/Generated.xcconfig")
+  if generated_xcode_build_settings.empty?
+    puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter build or flutter run is executed once first."
   end
+  generated_xcode_build_settings.map{ |p|
+    if p[:name]=='FLUTTER_FRAMEWORK_DIR'
+      pod 'Flutter', :path => p[:path]
+    end
+  }
+
+  # Plugin Pods
+  plugin_pods = parse_KV_file("../.flutter-plugins")
+  plugin_pods.map{ |p|
+    pod p[:name], :path => File.expand_path("ios",p[:path])
+  }
 end
 
 post_install do |installer|
@@ -33,4 +47,4 @@
       config.build_settings['ENABLE_BITCODE'] = 'NO'
     end
   end
-end
+end
\ No newline at end of file
diff --git a/packages/flutter_tools/templates/cocoapods/Podfile-swift b/packages/flutter_tools/templates/cocoapods/Podfile-swift
index 74b3de0..3bc469f 100644
--- a/packages/flutter_tools/templates/cocoapods/Podfile-swift
+++ b/packages/flutter_tools/templates/cocoapods/Podfile-swift
@@ -1,32 +1,44 @@
 # 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')
+def parse_KV_file(file,seperator='=')
+  file_abs_path = File.expand_path(file)
+  if !File.exists? file_abs_path
+    return [];
+  end
+  pods_ary = []
+  File.foreach(file_abs_path) { |line|
+      plugin = line.split(pattern=seperator)
+      if plugin.length == 2
+        podname = plugin[0].strip()
+        path = plugin[1].strip()
+        podpath = File.expand_path("#{path}", file_abs_path)
+        pods_ary.push({:name => podname,:path=>podpath});
+      else
+        puts "Invalid plugin specification: #{line}"
+      end
+  }
+  return pods_ary
 end
 
 target 'Runner' do
   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
-    }
+  generated_xcode_build_settings = parse_KV_file("./Flutter/Generated.xcconfig")
+  if generated_xcode_build_settings.empty?
+    puts "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter build or flutter run is executed once first."
   end
+  generated_xcode_build_settings.map{ |p|
+    if p[:name]=='FLUTTER_FRAMEWORK_DIR'
+      pod 'Flutter', :path => p[:path]
+    end
+  }
+
+  # Plugin Pods
+  plugin_pods = parse_KV_file("../.flutter-plugins")
+  plugin_pods.map{ |p|
+    pod p[:name], :path => File.expand_path("ios",p[:path])
+  }
 end
 
 post_install do |installer|
@@ -35,4 +47,4 @@
       config.build_settings['ENABLE_BITCODE'] = 'NO'
     end
   end
-end
+end
\ No newline at end of file
diff --git a/packages/flutter_tools/test/ios/cocoapods_test.dart b/packages/flutter_tools/test/ios/cocoapods_test.dart
index 493468f..e010d1a 100644
--- a/packages/flutter_tools/test/ios/cocoapods_test.dart
+++ b/packages/flutter_tools/test/ios/cocoapods_test.dart
@@ -95,7 +95,8 @@
       await cocoaPodsUnderTest.processPods(
         appIosDir: projectUnderTest,
         iosEngineDir: 'engine/path',
-      );      expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Existing Podfile');
+      );
+      expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Existing Podfile');
       verify(mockProcessManager.run(
         <String>['pod', 'install', '--verbose'],
         workingDirectory: 'project/ios',
@@ -177,6 +178,64 @@
       ProcessManager: () => mockProcessManager,
     },
   );
+
+  testUsingContext(
+    'Run pod install if plugins or flutter framework have changes.',
+        () async {
+      fs.file(fs.path.join('project', 'ios', 'Podfile'))
+        ..createSync()
+        ..writeAsString('Existing Podfile');
+      fs.file(fs.path.join('project', 'ios', 'Podfile.lock'))
+        ..createSync()
+        ..writeAsString('Existing lock files.');
+      fs.file(fs.path.join('project', 'ios', 'Pods','Manifest.lock'))
+        ..createSync(recursive: true)
+        ..writeAsString('Existing lock files.');
+      await cocoaPodsUnderTest.processPods(
+          appIosDir: projectUnderTest,
+          iosEngineDir: 'engine/path',
+          pluginOrFlutterPodChanged: true
+      );
+      verify(mockProcessManager.run(
+        <String>['pod', 'install', '--verbose'],
+        workingDirectory: 'project/ios',
+        environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'},
+      ));
+    },
+    overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    },
+  );
+
+  testUsingContext(
+    'Skip pod install if plugins and flutter framework remain unchanged.',
+        () async {
+      fs.file(fs.path.join('project', 'ios', 'Podfile'))
+        ..createSync()
+        ..writeAsString('Existing Podfile');
+      fs.file(fs.path.join('project', 'ios', 'Podfile.lock'))
+        ..createSync()
+        ..writeAsString('Existing lock files.');
+      fs.file(fs.path.join('project', 'ios', 'Pods','Manifest.lock'))
+        ..createSync(recursive: true)
+        ..writeAsString('Existing lock files.');
+      await cocoaPodsUnderTest.processPods(
+          appIosDir: projectUnderTest,
+          iosEngineDir: 'engine/path',
+          pluginOrFlutterPodChanged: false
+      );
+      verifyNever(mockProcessManager.run(
+        <String>['pod', 'install', '--verbose'],
+        workingDirectory: 'project/ios',
+        environment: <String, String>{'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'},
+      ));
+    },
+    overrides: <Type, Generator>{
+      FileSystem: () => fs,
+      ProcessManager: () => mockProcessManager,
+    },
+  );
 }
 
 class MockProcessManager extends Mock implements ProcessManager {}