cocoapods.dart 16.7 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
import '../base/process.dart';
16
import '../base/project_migrator.dart';
17
import '../base/version.dart';
18
import '../build_info.dart';
19
import '../cache.dart';
20
import '../ios/xcodeproj.dart';
21
import '../migrations/cocoapods_script_symlink.dart';
22
import '../reporting/reporting.dart';
23
import '../xcode_project.dart';
24

25
const String noCocoaPodsConsequence = '''
26 27
  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.
28
  For more info, see https://flutter.dev/platform-plugins''';
29

30 31 32 33
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.''';

34 35 36
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.
37
  This can usually be fixed by re-installing CocoaPods.''';
38

39
const String outOfDateFrameworksPodfileConsequence = '''
40 41 42
  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.''';

43
const String outOfDatePluginsPodfileConsequence = '''
44
  This can cause issues if your application depends on plugins that do not support iOS or macOS.
45 46 47
  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.''';

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

50
const String podfileIosMigrationInstructions = '''
51 52
  rm ios/Podfile''';

53 54 55
const String podfileMacOSMigrationInstructions = '''
  rm macos/Podfile''';

56 57 58 59
/// Result of evaluating the CocoaPods installation.
enum CocoaPodsStatus {
  /// iOS plugins will not work, installation required.
  notInstalled,
60 61
  /// iOS plugins might not work, upgrade recommended.
  unknownVersion,
62 63 64 65 66 67 68
  /// 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,
69 70
  /// iOS plugins will not work, re-install required.
  brokenInstall,
71
}
72

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

76
/// Cocoapods is a dependency management solution for iOS and macOS applications.
77 78 79 80 81 82 83 84
///
/// 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.
85
class CocoaPods {
86
  CocoaPods({
87 88 89 90 91 92
    required FileSystem fileSystem,
    required ProcessManager processManager,
    required XcodeProjectInterpreter xcodeProjectInterpreter,
    required Logger logger,
    required Platform platform,
    required Usage usage,
93 94 95 96
  }) : _fileSystem = fileSystem,
      _processManager = processManager,
      _xcodeProjectInterpreter = xcodeProjectInterpreter,
      _logger = logger,
97
      _usage = usage,
98
      _processUtils = ProcessUtils(processManager: processManager, logger: logger),
99 100 101 102 103 104
      _operatingSystemUtils = OperatingSystemUtils(
        fileSystem: fileSystem,
        logger: logger,
        platform: platform,
        processManager: processManager,
      );
105 106 107 108

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

114
  Future<String?>? _versionText;
115

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

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

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

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

      // This migrator works around a CocoaPods bug, and should be run after `pod install` is run.
      final ProjectMigration postPodMigration = ProjectMigration(<ProjectMigrator>[
        CocoaPodsScriptReadlink(xcodeProject, _xcodeProjectInterpreter, _logger),
      ]);
      postPodMigration.run();

178
      podsProcessed = true;
179
    }
180
    return podsProcessed;
181 182
  }

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

    return true;
  }

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

271 272 273 274 275
  /// 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');
276 277
  }

278 279
  void _addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject, String mode) {
    final File file = xcodeProject.xcodeConfigFor(mode);
280 281
    if (file.existsSync()) {
      final String content = file.readAsStringSync();
282 283 284
      final String includeFile = 'Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode
          .toLowerCase()}.xcconfig';
      final String include = '#include? "$includeFile"';
285
      if (!content.contains('Pods/Target Support Files/Pods-')) {
286
        file.writeAsStringSync('$include\n$content', flush: true);
287
      }
288 289 290 291
    }
  }

  /// Ensures that pod install is deemed needed on next check.
292 293
  void invalidatePodInstallOutput(XcodeBasedProject xcodeProject) {
    final File manifestLock = xcodeProject.podManifestLock;
294
    ErrorHandlingFileSystem.deleteIfExists(manifestLock);
295 296
  }

297 298
  // Check if you need to run pod install.
  // The pod install will run if any of below is true.
299 300 301 302
  // 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.
303
  bool _shouldRunPodInstall(XcodeBasedProject xcodeProject, bool dependenciesChanged) {
304
    if (dependenciesChanged) {
305
      return true;
306
    }
307

308 309 310
    final File podfileFile = xcodeProject.podfile;
    final File podfileLockFile = xcodeProject.podfileLock;
    final File manifestLockFile = xcodeProject.podManifestLock;
311

312
    return !podfileLockFile.existsSync()
313
        || !manifestLockFile.existsSync()
314
        || podfileLockFile.statSync().modified.isBefore(podfileFile.statSync().modified)
315
        || podfileLockFile.readAsStringSync() != manifestLockFile.readAsStringSync();
316 317
  }

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

344
    if (result.exitCode != 0) {
345
      invalidatePodInstallOutput(xcodeProject);
346
      _diagnosePodInstallFailure(result);
347
      throwToolExit('Error running pod install');
348 349 350 351 352 353 354
    } 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),
      );
355 356 357 358
    }
  }

  void _diagnosePodInstallFailure(ProcessResult result) {
359 360 361
    final Object? stdout = result.stdout;
    final Object? stderr = result.stderr;
    if (stdout is! String || stderr is! String) {
362 363 364
      return;
    }
    if (stdout.contains('out-of-date source repos')) {
365
      _logger.printError(
366 367 368 369 370
        "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,
      );
371
    } else if ((_isFfiX86Error(stdout) || _isFfiX86Error(stderr)) &&
372
        _operatingSystemUtils.hostPlatform == HostPlatform.darwin_arm64) {
373 374 375 376 377 378 379 380
      // 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'
381
        '  sudo gem uninstall ffi && sudo gem install ffi -- --enable-libffi-alloc\n',
382 383
        emphasis: true,
      );
384
    }
385
  }
386

387 388 389 390
  bool _isFfiX86Error(String error) {
    return error.contains('ffi_c.bundle') || error.contains('/ffi/');
  }

391
  void _warnIfPodfileOutOfDate(XcodeBasedProject xcodeProject) {
392 393 394 395 396 397 398 399 400 401
    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(
402
        xcodeProject.symlinks.path,
403 404 405 406 407 408 409 410 411 412
        'flutter',
      ));
      if (flutterSymlink.existsSync()) {
        throwToolExit(
          'Warning: Podfile is out of date\n'
              '$outOfDateFrameworksPodfileConsequence\n'
              'To regenerate the Podfile, run:\n'
              '$podfileIosMigrationInstructions\n',
        );
      }
413 414 415 416 417 418
    }
    // 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() &&
419
      xcodeProject.podfile.readAsStringSync().contains(".flutter-plugins'")) {
420
      const String warning = 'Warning: Podfile is out of date\n'
421 422 423
          '$outOfDatePluginsPodfileConsequence\n'
          'To regenerate the Podfile, run:\n';
      if (isIos) {
424
        throwToolExit('$warning\n$podfileIosMigrationInstructions\n');
425 426 427
      } else {
        // The old macOS Podfile will work until `.flutter-plugins` is removed.
        // Warn instead of exit.
428
        _logger.printWarning('$warning\n$podfileMacOSMigrationInstructions\n', emphasis: true);
429
      }
430 431
    }
  }
432
}