cocoapods.dart 17.1 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:meta/meta.dart';
7
import 'package:process/process.dart';
8

9
import '../artifacts.dart';
10
import '../base/common.dart';
11
import '../base/error_handling_io.dart';
12 13 14
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
15
import '../base/os.dart';
16
import '../base/platform.dart';
17 18
import '../base/process.dart';
import '../base/version.dart';
19
import '../build_info.dart';
20
import '../cache.dart';
21
import '../ios/xcodeproj.dart';
22
import '../project.dart';
23
import '../reporting/reporting.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 44 45 46 47
const String outOfDatePluginsPodfileConsequence = '''
  This can cause issues if your application depends on plugins that do not support iOS.
  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 51 52
const String podfileMigrationInstructions = '''
  rm ios/Podfile''';

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

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

  final FileSystem _fileSystem;
  final ProcessManager _processManager;
  final FileSystemUtils _fileSystemUtils;
  final ProcessUtils _processUtils;
108
  final OperatingSystemUtils _operatingSystemUtils;
109 110 111
  final XcodeProjectInterpreter _xcodeProjectInterpreter;
  final Logger _logger;
  final Platform _platform;
112
  final Artifacts _artifacts;
113
  final Usage _usage;
114

115
  Future<String> _versionText;
116

117
  String get cocoaPodsMinimumVersion => '1.6.0';
118
  String get cocoaPodsRecommendedVersion => '1.9.0';
119

120
  Future<bool> get isInstalled =>
121
    _processUtils.exitsHappy(<String>['which', 'pod']);
122

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

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

160 161 162
  /// Whether CocoaPods ran 'pod setup' once where the costly pods' specs are
  /// cloned.
  ///
163 164 165 166
  /// Versions >= 1.8.0 do not require 'pod setup' and default to a CDN instead
  /// of a locally cloned repository.
  /// See http://blog.cocoapods.org/CocoaPods-1.8.0-beta/
  ///
167 168 169 170 171
  /// A user can override the default location via the CP_REPOS_DIR environment
  /// variable.
  ///
  /// See https://github.com/CocoaPods/CocoaPods/blob/master/lib/cocoapods/config.rb#L138
  /// for details of this variable.
172 173 174 175 176
  Future<bool> get isCocoaPodsInitialized async {
    final Version installedVersion = Version.parse(await cocoaPodsVersionText);
    if (installedVersion != null && installedVersion >= Version.parse('1.8.0')) {
      return true;
    }
177 178 179
    final String cocoapodsReposDir = _platform.environment['CP_REPOS_DIR']
      ?? _fileSystem.path.join(_fileSystemUtils.homeDirPath, '.cocoapods', 'repos');
    return _fileSystem.isDirectory(_fileSystem.path.join(cocoapodsReposDir, 'master'));
180
  }
181

182
  Future<bool> processPods({
183
    @required XcodeBasedProject xcodeProject,
184
    @required BuildMode buildMode,
185
    bool dependenciesChanged = true,
186
  }) async {
187
    if (!xcodeProject.podfile.existsSync()) {
188 189
      throwToolExit('Podfile missing');
    }
190
    bool podsProcessed = false;
191 192 193
    if (_shouldRunPodInstall(xcodeProject, dependenciesChanged)) {
      if (!await _checkPodCondition()) {
        throwToolExit('CocoaPods not installed or not in valid state.');
194
      }
195
      await _runPodInstall(xcodeProject, buildMode);
196
      podsProcessed = true;
197
    }
198
    _warnIfPodfileOutOfDate(xcodeProject);
199
    return podsProcessed;
200 201
  }

202
  /// Make sure the CocoaPods tools are in the right states.
203
  Future<bool> _checkPodCondition() async {
204 205 206
    final CocoaPodsStatus installation = await evaluateCocoaPodsInstallation;
    switch (installation) {
      case CocoaPodsStatus.notInstalled:
207
        _logger.printError(
208 209
          'Warning: CocoaPods not installed. Skipping pod install.\n'
          '$noCocoaPodsConsequence\n'
210
          'To install $cocoaPodsInstallInstructions\n',
211 212 213
          emphasis: true,
        );
        return false;
214
      case CocoaPodsStatus.brokenInstall:
215
        _logger.printError(
216 217
          'Warning: CocoaPods is installed but broken. Skipping pod install.\n'
          '$brokenCocoaPodsConsequence\n'
218
          'To re-install $cocoaPodsInstallInstructions\n',
219 220 221
          emphasis: true,
        );
        return false;
222
      case CocoaPodsStatus.unknownVersion:
223
        _logger.printError(
224 225
          'Warning: Unknown CocoaPods version installed.\n'
          '$unknownCocoaPodsConsequence\n'
226
          'To upgrade $cocoaPodsInstallInstructions\n',
227 228 229
          emphasis: true,
        );
        break;
230
      case CocoaPodsStatus.belowMinimumVersion:
231
        _logger.printError(
232 233
          'Warning: CocoaPods minimum required version $cocoaPodsMinimumVersion or greater not installed. Skipping pod install.\n'
          '$noCocoaPodsConsequence\n'
234
          'To upgrade $cocoaPodsInstallInstructions\n',
235 236 237 238
          emphasis: true,
        );
        return false;
      case CocoaPodsStatus.belowRecommendedVersion:
239
        _logger.printError(
240 241
          'Warning: CocoaPods recommended version $cocoaPodsRecommendedVersion or greater not installed.\n'
          'Pods handling may fail on some projects involving plugins.\n'
242
          'To upgrade $cocoaPodsInstallInstructions\n',
243 244 245
          emphasis: true,
        );
        break;
246
      case CocoaPodsStatus.recommended:
247
        break;
248 249
    }
    if (!await isCocoaPodsInitialized) {
250
      _logger.printError(
251 252 253 254
        'Warning: CocoaPods installed but not initialized. Skipping pod install.\n'
        '$noCocoaPodsConsequence\n'
        'To initialize CocoaPods, run:\n'
        '  pod setup\n'
255
        "once to finalize CocoaPods' installation.",
256 257 258 259 260 261 262 263
        emphasis: true,
      );
      return false;
    }

    return true;
  }

264
  /// Ensures the given Xcode-based sub-project of a parent Flutter project
265 266
  /// contains a suitable `Podfile` and that its `Flutter/Xxx.xcconfig` files
  /// include pods configuration.
267
  Future<void> setupPodfile(XcodeBasedProject xcodeProject) async {
268
    if (!_xcodeProjectInterpreter.isInstalled) {
269 270 271
      // Don't do anything for iOS when host platform doesn't support it.
      return;
    }
272
    final Directory runnerProject = xcodeProject.xcodeProject;
273
    if (!runnerProject.existsSync()) {
274 275
      return;
    }
276
    final File podfile = xcodeProject.podfile;
277 278 279 280 281 282 283 284
    if (podfile.existsSync()) {
      addPodsDependencyToFlutterXcconfig(xcodeProject);
      return;
    }
    String podfileTemplateName;
    if (xcodeProject is MacOSProject) {
      podfileTemplateName = 'Podfile-macos';
    } else {
285
      final bool isSwift = (await _xcodeProjectInterpreter.getBuildSettings(
286 287 288
        runnerProject.path,
      )).containsKey('SWIFT_VERSION');
      podfileTemplateName = isSwift ? 'Podfile-ios-swift' : 'Podfile-ios-objc';
289
    }
290
    final File podfileTemplate = _fileSystem.file(_fileSystem.path.join(
291 292 293 294 295 296 297 298
      Cache.flutterRoot,
      'packages',
      'flutter_tools',
      'templates',
      'cocoapods',
      podfileTemplateName,
    ));
    podfileTemplate.copySync(podfile.path);
299
    addPodsDependencyToFlutterXcconfig(xcodeProject);
300 301
  }

302 303 304 305 306
  /// 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');
307 308
  }

309 310
  void _addPodsDependencyToFlutterXcconfig(XcodeBasedProject xcodeProject, String mode) {
    final File file = xcodeProject.xcodeConfigFor(mode);
311 312 313 314
    if (file.existsSync()) {
      final String content = file.readAsStringSync();
      final String include = '#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.${mode
          .toLowerCase()}.xcconfig"';
315
      if (!content.contains(include)) {
316
        file.writeAsStringSync('$include\n$content', flush: true);
317
      }
318 319 320 321
    }
  }

  /// Ensures that pod install is deemed needed on next check.
322 323
  void invalidatePodInstallOutput(XcodeBasedProject xcodeProject) {
    final File manifestLock = xcodeProject.podManifestLock;
324
    ErrorHandlingFileSystem.deleteIfExists(manifestLock);
325 326
  }

327 328
  // Check if you need to run pod install.
  // The pod install will run if any of below is true.
329 330 331 332
  // 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.
333
  bool _shouldRunPodInstall(XcodeBasedProject xcodeProject, bool dependenciesChanged) {
334
    if (dependenciesChanged) {
335
      return true;
336
    }
337

338 339 340
    final File podfileFile = xcodeProject.podfile;
    final File podfileLockFile = xcodeProject.podfileLock;
    final File manifestLockFile = xcodeProject.podManifestLock;
341

342
    return !podfileLockFile.existsSync()
343
        || !manifestLockFile.existsSync()
344
        || podfileLockFile.statSync().modified.isBefore(podfileFile.statSync().modified)
345
        || podfileLockFile.readAsStringSync() != manifestLockFile.readAsStringSync();
346 347
  }

348
  Future<void> _runPodInstall(XcodeBasedProject xcodeProject, BuildMode buildMode) async {
349
    final Status status = _logger.startProgress('Running pod install...');
350
    final ProcessResult result = await _processManager.run(
351
      <String>['pod', 'install', '--verbose'],
352
      workingDirectory: _fileSystem.path.dirname(xcodeProject.podfile.path),
353
      environment: <String, String>{
354 355 356 357
        // For macOS Podfile only.
        if (xcodeProject is MacOSProject)
          'FLUTTER_FRAMEWORK_DIR':
              flutterMacOSFrameworkDir(buildMode, _fileSystem, _artifacts),
358 359 360
        // See https://github.com/flutter/flutter/issues/10873.
        // CocoaPods analytics adds a lot of latency.
        'COCOAPODS_DISABLE_STATS': 'true',
361
        'LANG': 'en_US.UTF-8',
362
      },
363 364
    );
    status.stop();
365
    if (_logger.isVerbose || result.exitCode != 0) {
366 367
      final String stdout = result.stdout as String;
      if (stdout.isNotEmpty) {
368 369
        _logger.printStatus("CocoaPods' output:\n↳");
        _logger.printStatus(stdout, indent: 4);
370
      }
371 372
      final String stderr = result.stderr as String;
      if (stderr.isNotEmpty) {
373 374
        _logger.printStatus('Error output from CocoaPods:\n↳');
        _logger.printStatus(stderr, indent: 4);
375 376
      }
    }
377
    if (result.exitCode != 0) {
378
      invalidatePodInstallOutput(xcodeProject);
379
      _diagnosePodInstallFailure(result);
380
      throwToolExit('Error running pod install');
381 382 383 384
    }
  }

  void _diagnosePodInstallFailure(ProcessResult result) {
385 386 387 388 389
    if (result.stdout is! String) {
      return;
    }
    final String stdout = result.stdout as String;
    if (stdout.contains('out-of-date source repos')) {
390
      _logger.printError(
391 392 393 394 395
        "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,
      );
396 397 398 399 400 401 402 403 404 405 406 407 408 409
    } else if (stdout.contains('Init_ffi_c') &&
        stdout.contains('symbol not found') &&
        _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'
        '  arch -x86_64 sudo gem install ffi\n',
        emphasis: true,
      );
410
    }
411
  }
412 413 414 415 416

  void _warnIfPodfileOutOfDate(XcodeBasedProject xcodeProject) {
    if (xcodeProject is! IosProject) {
      return;
    }
417 418 419 420 421 422 423 424

    // 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.
425
    final Link flutterSymlink = _fileSystem.link(_fileSystem.path.join(
426
      (xcodeProject as IosProject).symlinks.path,
427 428 429
      'flutter',
    ));
    if (flutterSymlink.existsSync()) {
430
      throwToolExit(
431
        'Warning: Podfile is out of date\n'
432 433 434 435 436 437 438 439 440 441 442 443
        '$outOfDateFrameworksPodfileConsequence\n'
        'To regenerate the Podfile, run:\n'
        '$podfileMigrationInstructions\n',
      );
      return;
    }
    // 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() &&
      xcodeProject.podfile.readAsStringSync().contains('.flutter-plugins\'')) {
444
      throwToolExit(
445 446
        'Warning: Podfile is out of date\n'
        '$outOfDatePluginsPodfileConsequence\n'
447 448 449 450 451
        'To regenerate the Podfile, run:\n'
        '$podfileMigrationInstructions\n',
      );
    }
  }
452
}