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:meta/meta.dart'; 8 9import '../base/common.dart'; 10import '../base/context.dart'; 11import '../base/file_system.dart'; 12import '../base/io.dart'; 13import '../base/logger.dart'; 14import '../base/platform.dart'; 15import '../base/process.dart'; 16import '../base/process_manager.dart'; 17import '../base/version.dart'; 18import '../cache.dart'; 19import '../globals.dart'; 20import '../ios/xcodeproj.dart'; 21import '../project.dart'; 22 23const String noCocoaPodsConsequence = ''' 24 CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side. 25 Without CocoaPods, plugins will not work on iOS or macOS. 26 For more info, see https://flutter.dev/platform-plugins'''; 27 28const String unknownCocoaPodsConsequence = ''' 29 Flutter is unable to determine the installed CocoaPods's version. 30 Ensure that the output of 'pod --version' contains only digits and . to be recognized by Flutter.'''; 31 32const String cocoaPodsInstallInstructions = ''' 33 sudo gem install cocoapods 34 pod setup'''; 35 36const String cocoaPodsUpgradeInstructions = ''' 37 sudo gem install cocoapods 38 pod setup'''; 39 40CocoaPods get cocoaPods => context.get<CocoaPods>(); 41 42/// Result of evaluating the CocoaPods installation. 43enum CocoaPodsStatus { 44 /// iOS plugins will not work, installation required. 45 notInstalled, 46 /// iOS plugins might not work, upgrade recommended. 47 unknownVersion, 48 /// iOS plugins will not work, upgrade required. 49 belowMinimumVersion, 50 /// iOS plugins may not work in certain situations (Swift, static libraries), 51 /// upgrade recommended. 52 belowRecommendedVersion, 53 /// Everything should be fine. 54 recommended, 55} 56 57class CocoaPods { 58 Future<String> _versionText; 59 60 String get cocoaPodsMinimumVersion => '1.6.0'; 61 String get cocoaPodsRecommendedVersion => '1.6.0'; 62 63 Future<String> get cocoaPodsVersionText { 64 _versionText ??= runAsync(<String>['pod', '--version']).then<String>((RunResult result) { 65 return result.exitCode == 0 ? result.stdout.trim() : null; 66 }, onError: (dynamic _) => null); 67 return _versionText; 68 } 69 70 Future<CocoaPodsStatus> get evaluateCocoaPodsInstallation async { 71 final String versionText = await cocoaPodsVersionText; 72 if (versionText == null) 73 return CocoaPodsStatus.notInstalled; 74 try { 75 final Version installedVersion = Version.parse(versionText); 76 if (installedVersion == null) 77 return CocoaPodsStatus.unknownVersion; 78 if (installedVersion < Version.parse(cocoaPodsMinimumVersion)) 79 return CocoaPodsStatus.belowMinimumVersion; 80 else if (installedVersion < Version.parse(cocoaPodsRecommendedVersion)) 81 return CocoaPodsStatus.belowRecommendedVersion; 82 else 83 return CocoaPodsStatus.recommended; 84 } on FormatException { 85 return CocoaPodsStatus.notInstalled; 86 } 87 } 88 89 /// Whether CocoaPods ran 'pod setup' once where the costly pods' specs are 90 /// cloned. 91 /// 92 /// A user can override the default location via the CP_REPOS_DIR environment 93 /// variable. 94 /// 95 /// See https://github.com/CocoaPods/CocoaPods/blob/master/lib/cocoapods/config.rb#L138 96 /// for details of this variable. 97 Future<bool> get isCocoaPodsInitialized { 98 final String cocoapodsReposDir = platform.environment['CP_REPOS_DIR'] ?? fs.path.join(homeDirPath, '.cocoapods', 'repos'); 99 return fs.isDirectory(fs.path.join(cocoapodsReposDir, 'master')); 100 } 101 102 Future<bool> processPods({ 103 @required XcodeBasedProject xcodeProject, 104 // For backward compatibility with previously created Podfile only. 105 @required String engineDir, 106 bool isSwift = false, 107 bool dependenciesChanged = true, 108 }) async { 109 if (!(await xcodeProject.podfile.exists())) { 110 throwToolExit('Podfile missing'); 111 } 112 if (await _checkPodCondition()) { 113 if (_shouldRunPodInstall(xcodeProject, dependenciesChanged)) { 114 await _runPodInstall(xcodeProject, engineDir); 115 return true; 116 } 117 } 118 return false; 119 } 120 121 /// Make sure the CocoaPods tools are in the right states. 122 Future<bool> _checkPodCondition() async { 123 final CocoaPodsStatus installation = await evaluateCocoaPodsInstallation; 124 switch (installation) { 125 case CocoaPodsStatus.notInstalled: 126 printError( 127 'Warning: CocoaPods not installed. Skipping pod install.\n' 128 '$noCocoaPodsConsequence\n' 129 'To install:\n' 130 '$cocoaPodsInstallInstructions\n', 131 emphasis: true, 132 ); 133 return false; 134 case CocoaPodsStatus.unknownVersion: 135 printError( 136 'Warning: Unknown CocoaPods version installed.\n' 137 '$unknownCocoaPodsConsequence\n' 138 'To upgrade:\n' 139 '$cocoaPodsUpgradeInstructions\n', 140 emphasis: true, 141 ); 142 break; 143 case CocoaPodsStatus.belowMinimumVersion: 144 printError( 145 'Warning: CocoaPods minimum required version $cocoaPodsMinimumVersion or greater not installed. Skipping pod install.\n' 146 '$noCocoaPodsConsequence\n' 147 'To upgrade:\n' 148 '$cocoaPodsUpgradeInstructions\n', 149 emphasis: true, 150 ); 151 return false; 152 case CocoaPodsStatus.belowRecommendedVersion: 153 printError( 154 'Warning: CocoaPods recommended version $cocoaPodsRecommendedVersion or greater not installed.\n' 155 'Pods handling may fail on some projects involving plugins.\n' 156 'To upgrade:\n' 157 '$cocoaPodsUpgradeInstructions\n', 158 emphasis: true, 159 ); 160 break; 161 default: 162 break; 163 } 164 if (!await isCocoaPodsInitialized) { 165 printError( 166 'Warning: CocoaPods installed but not initialized. Skipping pod install.\n' 167 '$noCocoaPodsConsequence\n' 168 'To initialize CocoaPods, run:\n' 169 ' pod setup\n' 170 'once to finalize CocoaPods\' installation.', 171 emphasis: true, 172 ); 173 return false; 174 } 175 176 return true; 177 } 178 179 /// Ensures the given Xcode-based sub-project of a parent Flutter project 180 /// contains a suitable `Podfile` and that its `Flutter/Xxx.xcconfig` files 181 /// include pods configuration. 182 void setupPodfile(XcodeBasedProject xcodeProject) { 183 if (!xcodeProjectInterpreter.isInstalled) { 184 // Don't do anything for iOS when host platform doesn't support it. 185 return; 186 } 187 final Directory runnerProject = xcodeProject.xcodeProject; 188 if (!runnerProject.existsSync()) { 189 return; 190 } 191 final File podfile = xcodeProject.podfile; 192 if (!podfile.existsSync()) { 193 String podfileTemplateName; 194 if (xcodeProject is MacOSProject) { 195 podfileTemplateName = 'Podfile-macos'; 196 } else { 197 final bool isSwift = xcodeProjectInterpreter.getBuildSettings( 198 runnerProject.path, 199 'Runner', 200 ).containsKey('SWIFT_VERSION'); 201 podfileTemplateName = isSwift ? 'Podfile-ios-swift' : 'Podfile-ios-objc'; 202 } 203 final File podfileTemplate = fs.file(fs.path.join( 204 Cache.flutterRoot, 205 'packages', 206 'flutter_tools', 207 'templates', 208 'cocoapods', 209 podfileTemplateName, 210 )); 211 podfileTemplate.copySync(podfile.path); 212 } 213 addPodsDependencyToFlutterXcconfig(xcodeProject); 214 } 215 216 /// Ensures all `Flutter/Xxx.xcconfig` files for the given Xcode-based 217 /// sub-project of a parent Flutter project include pods configuration. 218 void addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject) { 219 _addPodsDependencyToFlutterXcconfig(xcodeProject, 'Debug'); 220 _addPodsDependencyToFlutterXcconfig(xcodeProject, 'Release'); 221 } 222 223 void _addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject, String mode) { 224 final File file = xcodeProject.xcodeConfigFor(mode); 225 if (file.existsSync()) { 226 final String content = file.readAsStringSync(); 227 final String include = '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode 228 .toLowerCase()}.xcconfig"'; 229 if (!content.contains(include)) 230 file.writeAsStringSync('$include\n$content', flush: true); 231 } 232 } 233 234 /// Ensures that pod install is deemed needed on next check. 235 void invalidatePodInstallOutput(XcodeBasedProject xcodeProject) { 236 final File manifestLock = xcodeProject.podManifestLock; 237 if (manifestLock.existsSync()) { 238 manifestLock.deleteSync(); 239 } 240 } 241 242 // Check if you need to run pod install. 243 // The pod install will run if any of below is true. 244 // 1. Flutter dependencies have changed 245 // 2. Podfile.lock doesn't exist or is older than Podfile 246 // 3. Pods/Manifest.lock doesn't exist (It is deleted when plugins change) 247 // 4. Podfile.lock doesn't match Pods/Manifest.lock. 248 bool _shouldRunPodInstall(XcodeBasedProject xcodeProject, bool dependenciesChanged) { 249 if (dependenciesChanged) 250 return true; 251 252 final File podfileFile = xcodeProject.podfile; 253 final File podfileLockFile = xcodeProject.podfileLock; 254 final File manifestLockFile = xcodeProject.podManifestLock; 255 256 return !podfileLockFile.existsSync() 257 || !manifestLockFile.existsSync() 258 || podfileLockFile.statSync().modified.isBefore(podfileFile.statSync().modified) 259 || podfileLockFile.readAsStringSync() != manifestLockFile.readAsStringSync(); 260 } 261 262 Future<void> _runPodInstall(XcodeBasedProject xcodeProject, String engineDirectory) async { 263 final Status status = logger.startProgress('Running pod install...', timeout: timeoutConfiguration.slowOperation); 264 final ProcessResult result = await processManager.run( 265 <String>['pod', 'install', '--verbose'], 266 workingDirectory: fs.path.dirname(xcodeProject.podfile.path), 267 environment: <String, String>{ 268 // For backward compatibility with previously created Podfile only. 269 'FLUTTER_FRAMEWORK_DIR': engineDirectory, 270 // See https://github.com/flutter/flutter/issues/10873. 271 // CocoaPods analytics adds a lot of latency. 272 'COCOAPODS_DISABLE_STATS': 'true', 273 }, 274 ); 275 status.stop(); 276 if (logger.isVerbose || result.exitCode != 0) { 277 if (result.stdout.isNotEmpty) { 278 printStatus('CocoaPods\' output:\n↳'); 279 printStatus(result.stdout, indent: 4); 280 } 281 if (result.stderr.isNotEmpty) { 282 printStatus('Error output from CocoaPods:\n↳'); 283 printStatus(result.stderr, indent: 4); 284 } 285 } 286 if (result.exitCode != 0) { 287 invalidatePodInstallOutput(xcodeProject); 288 _diagnosePodInstallFailure(result); 289 throwToolExit('Error running pod install'); 290 } 291 } 292 293 void _diagnosePodInstallFailure(ProcessResult result) { 294 if (result.stdout is String && result.stdout.contains('out-of-date source repos')) { 295 printError( 296 "Error: CocoaPods's specs repository is too out-of-date to satisfy dependencies.\n" 297 'To update the CocoaPods specs, run:\n' 298 ' pod repo update\n', 299 emphasis: true, 300 ); 301 } 302 } 303} 304