cocoapods.dart 16.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:file/file.dart';
6
import 'package:process/process.dart';
7 8

import '../base/common.dart';
9
import '../base/error_handling_io.dart';
10 11 12
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
13
import '../base/os.dart';
14
import '../base/platform.dart';
15 16
import '../base/process.dart';
import '../base/version.dart';
17
import '../build_info.dart';
18
import '../cache.dart';
19
import '../ios/xcodeproj.dart';
20
import '../reporting/reporting.dart';
21
import '../xcode_project.dart';
22

23
const String noCocoaPodsConsequence = '''
24 25
  CocoaPods is used to retrieve the iOS and macOS platform side's plugin code that responds to your plugin usage on the Dart side.
  Without CocoaPods, plugins will not work on iOS or macOS.
26
  For more info, see https://flutter.dev/platform-plugins''';
27

28 29 30 31
const String unknownCocoaPodsConsequence = '''
  Flutter is unable to determine the installed CocoaPods's version.
  Ensure that the output of 'pod --version' contains only digits and . to be recognized by Flutter.''';

32 33 34
const String brokenCocoaPodsConsequence = '''
  You appear to have CocoaPods installed but it is not working.
  This can happen if the version of Ruby that CocoaPods was installed with is different from the one being used to invoke it.
35
  This can usually be fixed by re-installing CocoaPods.''';
36

37
const String outOfDateFrameworksPodfileConsequence = '''
38 39 40
  This can cause a mismatched version of Flutter to be embedded in your app, which may result in App Store submission rejection or crashes.
  If you have local Podfile edits you would like to keep, see https://github.com/flutter/flutter/issues/24641 for instructions.''';

41
const String outOfDatePluginsPodfileConsequence = '''
42
  This can cause issues if your application depends on plugins that do not support iOS or macOS.
43 44 45
  See https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin-platforms for details.
  If you have local Podfile edits you would like to keep, see https://github.com/flutter/flutter/issues/45197 for instructions.''';

46
const String cocoaPodsInstallInstructions = 'see https://guides.cocoapods.org/using/getting-started.html#installation for instructions.';
47

48
const String podfileIosMigrationInstructions = '''
49 50
  rm ios/Podfile''';

51 52 53
const String podfileMacOSMigrationInstructions = '''
  rm macos/Podfile''';

54 55 56 57
/// Result of evaluating the CocoaPods installation.
enum CocoaPodsStatus {
  /// iOS plugins will not work, installation required.
  notInstalled,
58 59
  /// iOS plugins might not work, upgrade recommended.
  unknownVersion,
60 61 62 63 64 65 66
  /// iOS plugins will not work, upgrade required.
  belowMinimumVersion,
  /// iOS plugins may not work in certain situations (Swift, static libraries),
  /// upgrade recommended.
  belowRecommendedVersion,
  /// Everything should be fine.
  recommended,
67 68
  /// iOS plugins will not work, re-install required.
  brokenInstall,
69
}
70

71
const Version cocoaPodsMinimumVersion = Version.withText(1, 10, 0, '1.10.0');
72
const Version cocoaPodsRecommendedVersion = Version.withText(1, 11, 0, '1.11.0');
73

74
/// Cocoapods is a dependency management solution for iOS and macOS applications.
75 76 77 78 79 80 81 82
///
/// Cocoapods is generally installed via ruby gems and interacted with via
/// the `pod` CLI command.
///
/// See also:
///   * https://cocoapods.org/ - the cocoapods website.
///   * https://flutter.dev/docs/get-started/install/macos#deploy-to-ios-devices - instructions for
///     installing iOS/macOS dependencies.
83
class CocoaPods {
84
  CocoaPods({
85 86 87 88 89 90
    required FileSystem fileSystem,
    required ProcessManager processManager,
    required XcodeProjectInterpreter xcodeProjectInterpreter,
    required Logger logger,
    required Platform platform,
    required Usage usage,
91 92 93 94
  }) : _fileSystem = fileSystem,
      _processManager = processManager,
      _xcodeProjectInterpreter = xcodeProjectInterpreter,
      _logger = logger,
95
      _usage = usage,
96
      _processUtils = ProcessUtils(processManager: processManager, logger: logger),
97 98 99 100 101 102
      _operatingSystemUtils = OperatingSystemUtils(
        fileSystem: fileSystem,
        logger: logger,
        platform: platform,
        processManager: processManager,
      );
103 104 105 106

  final FileSystem _fileSystem;
  final ProcessManager _processManager;
  final ProcessUtils _processUtils;
107
  final OperatingSystemUtils _operatingSystemUtils;
108 109
  final XcodeProjectInterpreter _xcodeProjectInterpreter;
  final Logger _logger;
110
  final Usage _usage;
111

112
  Future<String?>? _versionText;
113

114
  Future<bool> get isInstalled =>
115
    _processUtils.exitsHappy(<String>['which', 'pod']);
116

117
  Future<String?> get cocoaPodsVersionText {
118
    _versionText ??= _processUtils.run(
119 120 121 122
      <String>['pod', '--version'],
      environment: <String, String>{
        'LANG': 'en_US.UTF-8',
      },
123
    ).then<String?>((RunResult result) {
124 125
      return result.exitCode == 0 ? result.stdout.trim() : null;
    }, onError: (dynamic _) => null);
126
    return _versionText!;
127
  }
128

129
  Future<CocoaPodsStatus> get evaluateCocoaPodsInstallation async {
130
    if (!(await isInstalled)) {
131
      return CocoaPodsStatus.notInstalled;
132
    }
133
    final String? versionText = await cocoaPodsVersionText;
134 135 136
    if (versionText == null) {
      return CocoaPodsStatus.brokenInstall;
    }
137
    try {
138
      final Version? installedVersion = Version.parse(versionText);
139
      if (installedVersion == null) {
140
        return CocoaPodsStatus.unknownVersion;
141
      }
142
      if (installedVersion < cocoaPodsMinimumVersion) {
143
        return CocoaPodsStatus.belowMinimumVersion;
144
      }
145
      if (installedVersion < cocoaPodsRecommendedVersion) {
146
        return CocoaPodsStatus.belowRecommendedVersion;
147 148
      }
      return CocoaPodsStatus.recommended;
149
    } on FormatException {
150
      return CocoaPodsStatus.notInstalled;
151 152 153
    }
  }

154
  Future<bool> processPods({
155 156
    required XcodeBasedProject xcodeProject,
    required BuildMode buildMode,
157
    bool dependenciesChanged = true,
158
  }) async {
159
    if (!xcodeProject.podfile.existsSync()) {
160 161
      throwToolExit('Podfile missing');
    }
162
    _warnIfPodfileOutOfDate(xcodeProject);
163
    bool podsProcessed = false;
164 165 166
    if (_shouldRunPodInstall(xcodeProject, dependenciesChanged)) {
      if (!await _checkPodCondition()) {
        throwToolExit('CocoaPods not installed or not in valid state.');
167
      }
168
      await _runPodInstall(xcodeProject, buildMode);
169
      podsProcessed = true;
170
    }
171
    return podsProcessed;
172 173
  }

174
  /// Make sure the CocoaPods tools are in the right states.
175
  Future<bool> _checkPodCondition() async {
176 177 178
    final CocoaPodsStatus installation = await evaluateCocoaPodsInstallation;
    switch (installation) {
      case CocoaPodsStatus.notInstalled:
179
        _logger.printWarning(
180 181
          'Warning: CocoaPods not installed. Skipping pod install.\n'
          '$noCocoaPodsConsequence\n'
182
          'To install $cocoaPodsInstallInstructions\n',
183 184 185
          emphasis: true,
        );
        return false;
186
      case CocoaPodsStatus.brokenInstall:
187
        _logger.printWarning(
188 189
          'Warning: CocoaPods is installed but broken. Skipping pod install.\n'
          '$brokenCocoaPodsConsequence\n'
190
          'To re-install $cocoaPodsInstallInstructions\n',
191 192 193
          emphasis: true,
        );
        return false;
194
      case CocoaPodsStatus.unknownVersion:
195
        _logger.printWarning(
196 197
          'Warning: Unknown CocoaPods version installed.\n'
          '$unknownCocoaPodsConsequence\n'
198
          'To upgrade $cocoaPodsInstallInstructions\n',
199 200 201
          emphasis: true,
        );
        break;
202
      case CocoaPodsStatus.belowMinimumVersion:
203
        _logger.printWarning(
204 205
          'Warning: CocoaPods minimum required version $cocoaPodsMinimumVersion or greater not installed. Skipping pod install.\n'
          '$noCocoaPodsConsequence\n'
206
          'To upgrade $cocoaPodsInstallInstructions\n',
207 208 209 210
          emphasis: true,
        );
        return false;
      case CocoaPodsStatus.belowRecommendedVersion:
211
        _logger.printWarning(
212 213
          'Warning: CocoaPods recommended version $cocoaPodsRecommendedVersion or greater not installed.\n'
          'Pods handling may fail on some projects involving plugins.\n'
214
          'To upgrade $cocoaPodsInstallInstructions\n',
215 216 217
          emphasis: true,
        );
        break;
218
      case CocoaPodsStatus.recommended:
219
        break;
220 221 222 223 224
    }

    return true;
  }

225
  /// Ensures the given Xcode-based sub-project of a parent Flutter project
226 227
  /// contains a suitable `Podfile` and that its `Flutter/Xxx.xcconfig` files
  /// include pods configuration.
228
  Future<void> setupPodfile(XcodeBasedProject xcodeProject) async {
229
    if (!_xcodeProjectInterpreter.isInstalled) {
230 231 232
      // Don't do anything for iOS when host platform doesn't support it.
      return;
    }
233
    final Directory runnerProject = xcodeProject.xcodeProject;
234
    if (!runnerProject.existsSync()) {
235 236
      return;
    }
237
    final File podfile = xcodeProject.podfile;
238 239 240 241 242 243 244 245
    if (podfile.existsSync()) {
      addPodsDependencyToFlutterXcconfig(xcodeProject);
      return;
    }
    String podfileTemplateName;
    if (xcodeProject is MacOSProject) {
      podfileTemplateName = 'Podfile-macos';
    } else {
246
      final bool isSwift = (await _xcodeProjectInterpreter.getBuildSettings(
247
        runnerProject.path,
248
        buildContext: const XcodeProjectBuildContext(),
249 250
      )).containsKey('SWIFT_VERSION');
      podfileTemplateName = isSwift ? 'Podfile-ios-swift' : 'Podfile-ios-objc';
251
    }
252
    final File podfileTemplate = _fileSystem.file(_fileSystem.path.join(
253
      Cache.flutterRoot!,
254 255 256 257 258 259 260
      'packages',
      'flutter_tools',
      'templates',
      'cocoapods',
      podfileTemplateName,
    ));
    podfileTemplate.copySync(podfile.path);
261
    addPodsDependencyToFlutterXcconfig(xcodeProject);
262 263
  }

264 265 266 267 268
  /// Ensures all `Flutter/Xxx.xcconfig` files for the given Xcode-based
  /// sub-project of a parent Flutter project include pods configuration.
  void addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject) {
    _addPodsDependencyToFlutterXcconfig(xcodeProject, 'Debug');
    _addPodsDependencyToFlutterXcconfig(xcodeProject, 'Release');
269 270
  }

271 272
  void _addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject, String mode) {
    final File file = xcodeProject.xcodeConfigFor(mode);
273 274
    if (file.existsSync()) {
      final String content = file.readAsStringSync();
275 276 277
      final String includeFile = 'Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode
          .toLowerCase()}.xcconfig';
      final String include = '#include? "$includeFile"';
278
      if (!content.contains('Pods/Target Support Files/Pods-')) {
279
        file.writeAsStringSync('$include\n$content', flush: true);
280
      }
281 282 283 284
    }
  }

  /// Ensures that pod install is deemed needed on next check.
285 286
  void invalidatePodInstallOutput(XcodeBasedProject xcodeProject) {
    final File manifestLock = xcodeProject.podManifestLock;
287
    ErrorHandlingFileSystem.deleteIfExists(manifestLock);
288 289
  }

290 291
  // Check if you need to run pod install.
  // The pod install will run if any of below is true.
292 293 294 295
  // 1. Flutter dependencies have changed
  // 2. Podfile.lock doesn't exist or is older than Podfile
  // 3. Pods/Manifest.lock doesn't exist (It is deleted when plugins change)
  // 4. Podfile.lock doesn't match Pods/Manifest.lock.
296
  bool _shouldRunPodInstall(XcodeBasedProject xcodeProject, bool dependenciesChanged) {
297
    if (dependenciesChanged) {
298
      return true;
299
    }
300

301 302 303
    final File podfileFile = xcodeProject.podfile;
    final File podfileLockFile = xcodeProject.podfileLock;
    final File manifestLockFile = xcodeProject.podManifestLock;
304

305
    return !podfileLockFile.existsSync()
306
        || !manifestLockFile.existsSync()
307
        || podfileLockFile.statSync().modified.isBefore(podfileFile.statSync().modified)
308
        || podfileLockFile.readAsStringSync() != manifestLockFile.readAsStringSync();
309 310
  }

311
  Future<void> _runPodInstall(XcodeBasedProject xcodeProject, BuildMode buildMode) async {
312
    final Status status = _logger.startProgress('Running pod install...');
313
    final ProcessResult result = await _processManager.run(
314
      <String>['pod', 'install', '--verbose'],
315
      workingDirectory: _fileSystem.path.dirname(xcodeProject.podfile.path),
316 317 318 319
      environment: <String, String>{
        // See https://github.com/flutter/flutter/issues/10873.
        // CocoaPods analytics adds a lot of latency.
        'COCOAPODS_DISABLE_STATS': 'true',
320
        'LANG': 'en_US.UTF-8',
321
      },
322 323
    );
    status.stop();
324
    if (_logger.isVerbose || result.exitCode != 0) {
325 326
      final String stdout = result.stdout as String;
      if (stdout.isNotEmpty) {
327 328
        _logger.printStatus("CocoaPods' output:\n↳");
        _logger.printStatus(stdout, indent: 4);
329
      }
330 331
      final String stderr = result.stderr as String;
      if (stderr.isNotEmpty) {
332 333
        _logger.printStatus('Error output from CocoaPods:\n↳');
        _logger.printStatus(stderr, indent: 4);
334 335
      }
    }
336

337
    if (result.exitCode != 0) {
338
      invalidatePodInstallOutput(xcodeProject);
339
      _diagnosePodInstallFailure(result);
340
      throwToolExit('Error running pod install');
341 342 343 344 345 346 347
    } else if (xcodeProject.podfileLock.existsSync()) {
      // Even if the Podfile.lock didn't change, update its modified date to now
      // so Podfile.lock is newer than Podfile.
      _processManager.runSync(
        <String>['touch', xcodeProject.podfileLock.path],
        workingDirectory: _fileSystem.path.dirname(xcodeProject.podfile.path),
      );
348 349 350 351
    }
  }

  void _diagnosePodInstallFailure(ProcessResult result) {
352 353 354
    final Object? stdout = result.stdout;
    final Object? stderr = result.stderr;
    if (stdout is! String || stderr is! String) {
355 356 357
      return;
    }
    if (stdout.contains('out-of-date source repos')) {
358
      _logger.printError(
359 360 361 362 363
        "Error: CocoaPods's specs repository is too out-of-date to satisfy dependencies.\n"
        'To update the CocoaPods specs, run:\n'
        '  pod repo update\n',
        emphasis: true,
      );
364
    } else if ((stderr.contains('ffi_c.bundle') || stderr.contains('/ffi/')) &&
365 366 367 368 369 370 371 372 373
        _operatingSystemUtils.hostPlatform == HostPlatform.darwin_arm) {
      // https://github.com/flutter/flutter/issues/70796
      UsageEvent(
        'pod-install-failure',
        'arm-ffi',
        flutterUsage: _usage,
      ).send();
      _logger.printError(
        'Error: To set up CocoaPods for ARM macOS, run:\n'
374
        '  sudo gem uninstall ffi && sudo gem install ffi -- --enable-libffi-alloc\n',
375 376
        emphasis: true,
      );
377
    }
378
  }
379 380

  void _warnIfPodfileOutOfDate(XcodeBasedProject xcodeProject) {
381 382 383 384 385 386 387 388 389 390
    final bool isIos = xcodeProject is IosProject;
    if (isIos) {
      // Previously, the Podfile created a symlink to the cached artifacts engine framework
      // and installed the Flutter pod from that path. This could get out of sync with the copy
      // of the Flutter engine that was copied to ios/Flutter by the xcode_backend script.
      // It was possible for the symlink to point to a Debug version of the engine when the
      // Xcode build configuration was Release, which caused App Store submission rejections.
      //
      // Warn the user if they are still symlinking to the framework.
      final Link flutterSymlink = _fileSystem.link(_fileSystem.path.join(
391
        xcodeProject.symlinks.path,
392 393 394 395 396 397 398 399 400 401
        'flutter',
      ));
      if (flutterSymlink.existsSync()) {
        throwToolExit(
          'Warning: Podfile is out of date\n'
              '$outOfDateFrameworksPodfileConsequence\n'
              'To regenerate the Podfile, run:\n'
              '$podfileIosMigrationInstructions\n',
        );
      }
402 403 404 405 406 407
    }
    // Most of the pod and plugin parsing logic was moved from the Podfile
    // into the tool's podhelper.rb script. If the Podfile still references
    // the old parsed .flutter-plugins file, prompt the regeneration. Old line was:
    // plugin_pods = parse_KV_file('../.flutter-plugins')
    if (xcodeProject.podfile.existsSync() &&
408
      xcodeProject.podfile.readAsStringSync().contains(".flutter-plugins'")) {
409
      const String warning = 'Warning: Podfile is out of date\n'
410 411 412
          '$outOfDatePluginsPodfileConsequence\n'
          'To regenerate the Podfile, run:\n';
      if (isIos) {
413
        throwToolExit('$warning\n$podfileIosMigrationInstructions\n');
414 415 416
      } else {
        // The old macOS Podfile will work until `.flutter-plugins` is removed.
        // Warn instead of exit.
417
        _logger.printWarning('$warning\n$podfileMacOSMigrationInstructions\n', emphasis: true);
418
      }
419 420
    }
  }
421
}