• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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