diff --git a/packages/flutter_tools/lib/src/commands/inject_plugins.dart b/packages/flutter_tools/lib/src/commands/inject_plugins.dart index bf53cb5164d..f1df432e227 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 @@ class InjectPluginsCommand extends FlutterCommand { @override Future 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 78fd425b836..f935821ea16 100644 --- a/packages/flutter_tools/lib/src/ios/cocoapods.dart +++ b/packages/flutter_tools/lib/src/ios/cocoapods.dart @@ -57,15 +57,17 @@ class CocoaPods { Future processPods({ @required Directory appIosDir, - @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 (_checkIfRunPodInstall(appIosDir.path, pluginOrFlutterPodChanged)) + await _runPodInstall(appIosDir); + } else { + throwToolExit('CocoaPods not available for project using Flutter plugins'); } } @@ -108,13 +110,12 @@ class CocoaPods { podfileTemplate.copySync(fs.path.join(bundle.path, 'Podfile')); } - Future _runPodInstall(Directory bundle, String engineDirectory) async { + Future _runPodInstall(Directory bundle) async { final Status status = logger.startProgress('Running pod install...', expectSlowOperation: true); final ProcessResult result = await processManager.run( ['pod', 'install', '--verbose'], workingDirectory: bundle.path, environment: { - 'FLUTTER_FRAMEWORK_DIR': engineDirectory, // See https://github.com/flutter/flutter/issues/10873. // CocoaPods analytics adds a lot of latency. 'COCOAPODS_DISABLE_STATS': 'true', @@ -137,6 +138,28 @@ class CocoaPods { } } + // 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 _checkIfRunPodInstall(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; + } + void _diagnosePodInstallFailure(ProcessResult result) { if (result.stdout is String && result.stdout.contains('out-of-date source repos')) { printError( diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index e3ead3bfeb4..00abe0e19e3 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -254,14 +254,9 @@ Future buildXcodeProject({ // 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 injectionResult = injectPlugins(); + final bool hasFlutterPlugins = injectionResult.hasPlugin; + final String priorGeneratedXCConfig = readGeneratedXCConfig(app.appDirectory); updateXcodeGeneratedProperties( projectPath: fs.currentDirectory.path, @@ -271,6 +266,15 @@ Future buildXcodeProject({ previewDart2: buildInfo.previewDart2, ); + if (hasFlutterPlugins) { + final String currentGeneratedXCConfig = readGeneratedXCConfig(app.appDirectory); + await cocoaPods.processPods( + appIosDir: appDirectory, + isSwift: app.isSwift, + pluginOrFlutterPodChanged: (injectionResult.hasChanged || + priorGeneratedXCConfig != currentGeneratedXCConfig)); + } + final List commands = [ '/usr/bin/env', 'xcrun', @@ -355,7 +359,17 @@ Future buildXcodeProject({ } } -Future diagnoseXcodeBuildFailure(XcodeBuildResult result, BuildableIOSApp app) async { +String readGeneratedXCConfig(String appPath) { + final String generateXCConfigPath = + fs.path.join(fs.currentDirectory.path, appPath, 'Flutter','Generated.xcconfig'); + final File generateXCConfigFile = fs.file(generateXCConfigPath); + if (!generateXCConfigFile.existsSync()) + return null; + return generateXCConfigFile.readAsStringSync(); +} + +Future diagnoseXcodeBuildFailure( + XcodeBuildResult result, BuildableIOSApp app) async { if (result.xcodeBuildExecution != null && result.xcodeBuildExecution.buildForPhysicalDevice && result.stdout?.contains('BCEROR') == true && diff --git a/packages/flutter_tools/lib/src/plugins.dart b/packages/flutter_tools/lib/src/plugins.dart index 19d7bc00415..0eeaac75ad0 100644 --- a/packages/flutter_tools/lib/src/plugins.dart +++ b/packages/flutter_tools/lib/src/plugins.dart @@ -77,8 +77,10 @@ List _findPlugins(String directory) { return plugins; } -void _writeFlutterPluginsList(String directory, List plugins) { +// Return true if .flutter-plugins has changed, otherwise return false. +bool _writeFlutterPluginsList(String directory, List plugins) { final File pluginsProperties = fs.file(fs.path.join(directory, '.flutter-plugins')); + final String priorFlutterPlugins = (pluginsProperties.existsSync() ? pluginsProperties.readAsStringSync() : null); final String pluginManifest = plugins.map((Plugin p) => '${p.name}=${escapePath(p.path)}').join('\n'); if (pluginManifest.isNotEmpty) { @@ -88,6 +90,8 @@ void _writeFlutterPluginsList(String directory, List plugins) { pluginsProperties.deleteSync(); } } + final String currentFlutterPlugins = (pluginsProperties.existsSync() ? pluginsProperties.readAsStringSync() : null); + return currentFlutterPlugins != priorFlutterPlugins; } const String _androidPluginRegistryTemplate = '''package io.flutter.plugins; @@ -206,17 +210,25 @@ void _writeIOSPluginRegistry(String directory, List plugins) { } +class InjectPluginsResult{ + InjectPluginsResult({this.hasPlugin : false, this.hasChanged : false}); + /// 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}) { +InjectPluginsResult injectPlugins({String directory}) { directory ??= fs.currentDirectory.path; final List 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); +} \ No newline at end of file diff --git a/packages/flutter_tools/templates/cocoapods/Podfile-objc b/packages/flutter_tools/templates/cocoapods/Podfile-objc index 90b5f651fb6..8350f1e55f9 100644 --- a/packages/flutter_tools/templates/cocoapods/Podfile-objc +++ b/packages/flutter_tools/templates/cocoapods/Podfile-objc @@ -1,30 +1,42 @@ # 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 + puts "Generated.xcconfig or .flutter-plugins expected but not present. If you're running pod install manually, make sure flutter build or flutter run is executed once first." + 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 - } - end + } + return pods_ary +end + +target 'Runner' do + use_frameworks! + # Flutter Pods + generated_xcode_build_settings = parse_KV_file("./Flutter/Generated.xcconfig") + 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 +45,4 @@ post_install do |installer| 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 74b3de06493..8350f1e55f9 100644 --- a/packages/flutter_tools/templates/cocoapods/Podfile-swift +++ b/packages/flutter_tools/templates/cocoapods/Podfile-swift @@ -1,32 +1,42 @@ # 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 + puts "Generated.xcconfig or .flutter-plugins expected but not present. If you're running pod install manually, make sure flutter build or flutter run is executed once first." + 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'] + generated_xcode_build_settings = parse_KV_file("./Flutter/Generated.xcconfig") + generated_xcode_build_settings.map{ |p| + if p[:name]=='FLUTTER_FRAMEWORK_DIR' + pod 'Flutter', :path => p[:path] + end + } - 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 + # 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 +45,4 @@ post_install do |installer| 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 493468fae8e..73c068d473a 100644 --- a/packages/flutter_tools/test/ios/cocoapods_test.dart +++ b/packages/flutter_tools/test/ios/cocoapods_test.dart @@ -41,7 +41,7 @@ void main() { when(mockProcessManager.run( ['pod', 'install', '--verbose'], workingDirectory: 'project/ios', - environment: {'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, + environment: {'COCOAPODS_DISABLE_STATS': 'true'}, )).thenReturn(exitsHappy); }); @@ -50,13 +50,12 @@ void main() { () async { await cocoaPodsUnderTest.processPods( appIosDir: projectUnderTest, - iosEngineDir: 'engine/path', ); expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Objective-C podfile template'); verify(mockProcessManager.run( ['pod', 'install', '--verbose'], workingDirectory: 'project/ios', - environment: {'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, + environment: {'COCOAPODS_DISABLE_STATS': 'true'}, )); }, overrides: { @@ -70,14 +69,13 @@ void main() { () async { await cocoaPodsUnderTest.processPods( appIosDir: projectUnderTest, - iosEngineDir: 'engine/path', isSwift: true, ); expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Swift podfile template'); verify(mockProcessManager.run( ['pod', 'install', '--verbose'], workingDirectory: 'project/ios', - environment: {'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, + environment: {'COCOAPODS_DISABLE_STATS': 'true'}, )); }, overrides: { @@ -94,12 +92,11 @@ void main() { ..writeAsString('Existing Podfile'); await cocoaPodsUnderTest.processPods( appIosDir: projectUnderTest, - iosEngineDir: 'engine/path', ); expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Existing Podfile'); verify(mockProcessManager.run( ['pod', 'install', '--verbose'], workingDirectory: 'project/ios', - environment: {'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, + environment: {'COCOAPODS_DISABLE_STATS': 'true'}, )); }, overrides: { @@ -115,14 +112,13 @@ void main() { try { await cocoaPodsUnderTest.processPods( appIosDir: projectUnderTest, - iosEngineDir: 'engine/path', ); fail('Expected tool error'); } catch (ToolExit) { verifyNever(mockProcessManager.run( ['pod', 'install', '--verbose'], workingDirectory: 'project/ios', - environment: {'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, + environment: {'COCOAPODS_DISABLE_STATS': 'true'}, )); } }, @@ -142,7 +138,7 @@ void main() { when(mockProcessManager.run( ['pod', 'install', '--verbose'], workingDirectory: 'project/ios', - environment: {'FLUTTER_FRAMEWORK_DIR': 'engine/path', 'COCOAPODS_DISABLE_STATS': 'true'}, + environment: {'COCOAPODS_DISABLE_STATS': 'true'}, )).thenReturn(new ProcessResult( 1, 1, @@ -165,7 +161,6 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by try { await cocoaPodsUnderTest.processPods( appIosDir: projectUnderTest, - iosEngineDir: 'engine/path', ); expect(fs.file(fs.path.join('project', 'ios', 'Podfile')).readAsStringSync() , 'Existing Podfile'); fail('Exception expected'); } catch (ToolExit) { @@ -177,6 +172,62 @@ Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by 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, + pluginOrFlutterPodChanged: true + ); + verify(mockProcessManager.run( + ['pod', 'install', '--verbose'], + workingDirectory: 'project/ios', + environment: {'COCOAPODS_DISABLE_STATS': 'true'}, + )); + }, + overrides: { + 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, + pluginOrFlutterPodChanged: false + ); + verifyNever(mockProcessManager.run( + ['pod', 'install', '--verbose'], + workingDirectory: 'project/ios', + environment: {'COCOAPODS_DISABLE_STATS': 'true'}, + )); + }, + overrides: { + FileSystem: () => fs, + ProcessManager: () => mockProcessManager, + }, + ); } class MockProcessManager extends Mock implements ProcessManager {}