ios_workflow.dart 8.37 KB
Newer Older
1 2 3 4
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6

7
import '../base/context.dart';
8
import '../base/os.dart';
9
import '../base/platform.dart';
10
import '../base/process.dart';
11
import '../base/version.dart';
12
import '../doctor.dart';
13
import 'cocoapods.dart';
14
import 'mac.dart';
15
import 'plist_utils.dart' as plist;
16

17
IOSWorkflow get iosWorkflow => context[IOSWorkflow];
18

19
class IOSWorkflow extends DoctorValidator implements Workflow {
20
  const IOSWorkflow() : super('iOS toolchain - develop for iOS devices');
21

22
  @override
23
  bool get appliesToHostPlatform => platform.isMacOS;
24

25
  // We need xcode (+simctl) to list simulator devices, and libimobiledevice to list real devices.
26
  @override
27
  bool get canListDevices => xcode.isInstalledAndMeetsVersionCheck && xcode.isSimctlInstalled;
28 29 30

  // We need xcode to launch simulator devices, and ideviceinstaller and ios-deploy
  // for real devices.
31
  @override
32 33
  bool get canLaunchDevices => xcode.isInstalledAndMeetsVersionCheck;

34 35 36
  @override
  bool get canListEmulators => false;

37 38 39 40
  String getPlistValueFromFile(String path, String key) {
    return plist.getValueFromFile(path, key);
  }

41
  Future<bool> get hasIDeviceInstaller => exitsHappyAsync(<String>['ideviceinstaller', '-h']);
42

43
  Future<bool> get hasIosDeploy => exitsHappyAsync(<String>['ios-deploy', '--version']);
44

45
  String get iosDeployMinimumVersion => '1.9.2';
46

47
  Future<String> get iosDeployVersionText async => (await runAsync(<String>['ios-deploy', '--version'])).processResult.stdout.replaceAll('\n', '');
48

49 50
  bool get hasHomebrew => os.which('brew') != null;

51 52
  Future<String> get macDevMode async => (await runAsync(<String>['DevToolsSecurity', '-status'])).processResult.stdout;

53 54
  Future<bool> get _iosDeployIsInstalledAndMeetsVersionCheck async {
    if (!await hasIosDeploy)
55 56
      return false;
    try {
57
      final Version version = new Version.parse(await iosDeployVersionText);
58 59 60 61 62 63
      return version >= new Version.parse(iosDeployMinimumVersion);
    } on FormatException catch (_) {
      return false;
    }
  }

64
  @override
65
  Future<ValidationResult> validate() async {
66
    final List<ValidationMessage> messages = <ValidationMessage>[];
67 68
    ValidationType xcodeStatus = ValidationType.missing;
    ValidationType brewStatus = ValidationType.missing;
69 70 71
    String xcodeVersionInfo;

    if (xcode.isInstalled) {
72
      xcodeStatus = ValidationType.installed;
73

74
      messages.add(new ValidationMessage('Xcode at ${xcode.xcodeSelectPath}'));
75

76
      xcodeVersionInfo = xcode.versionText;
77 78
      if (xcodeVersionInfo.contains(','))
        xcodeVersionInfo = xcodeVersionInfo.substring(0, xcodeVersionInfo.indexOf(','));
79
      messages.add(new ValidationMessage(xcode.versionText));
80 81

      if (!xcode.isInstalledAndMeetsVersionCheck) {
82
        xcodeStatus = ValidationType.partial;
83
        messages.add(new ValidationMessage.error(
84
          'Flutter requires a minimum Xcode version of $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor.0.\n'
85 86 87 88 89
          'Download the latest version or update via the Mac App Store.'
        ));
      }

      if (!xcode.eulaSigned) {
90
        xcodeStatus = ValidationType.partial;
91
        messages.add(new ValidationMessage.error(
92
          'Xcode end user license agreement not signed; open Xcode or run the command \'sudo xcodebuild -license\'.'
93 94
        ));
      }
95 96 97 98 99 100 101
      if (!xcode.isSimctlInstalled) {
        xcodeStatus = ValidationType.partial;
        messages.add(new ValidationMessage.error(
          'Xcode requires additional components to be installed in order to run.\n'
          'Launch Xcode and install additional required components when prompted.'
        ));
      }
102

103
    } else {
104
      xcodeStatus = ValidationType.missing;
105
      if (xcode.xcodeSelectPath == null || xcode.xcodeSelectPath.isEmpty) {
106 107 108 109 110 111 112
        messages.add(new ValidationMessage.error(
            'Xcode not installed; this is necessary for iOS development.\n'
            'Download at https://developer.apple.com/xcode/download/.'
        ));
      } else {
        messages.add(new ValidationMessage.error(
            'Xcode installation is incomplete; a full installation is necessary for iOS development.\n'
113
            'Download at: https://developer.apple.com/xcode/download/\n'
114 115 116
            'Or install Xcode via the App Store.\n'
            'Once installed, run:\n'
            '  sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer'
117 118
        ));
      }
119 120 121
    }

    // brew installed
122
    if (hasHomebrew) {
123
      brewStatus = ValidationType.installed;
124

125
      if (!iMobileDevice.isInstalled) {
126 127
        brewStatus = ValidationType.partial;
        messages.add(new ValidationMessage.error(
128 129 130 131 132 133 134
            'libimobiledevice and ideviceinstaller are not installed. To install, run:\n'
            '  brew install --HEAD libimobiledevice\n'
            '  brew install ideviceinstaller'
        ));
      } else if (!await iMobileDevice.isWorking) {
        brewStatus = ValidationType.partial;
        messages.add(new ValidationMessage.error(
135 136 137
            'Verify that all connected devices have been paired with this computer in Xcode.\n'
            'If all devices have been paired, libimobiledevice and ideviceinstaller may require updating.\n'
            'To update, run:\n'
138 139 140
            '  brew uninstall --ignore-dependencies libimobiledevice\n'
            '  brew install --HEAD libimobiledevice\n'
            '  brew install ideviceinstaller'
141
        ));
142
      } else if (!await hasIDeviceInstaller) {
143
        brewStatus = ValidationType.partial;
144
        messages.add(new ValidationMessage.error(
145
          'ideviceinstaller is not installed; this is used to discover connected iOS devices.\n'
146
          'To install, run:\n'
147 148
          '  brew install --HEAD libimobiledevice\n'
          '  brew install ideviceinstaller'
149 150 151
        ));
      }

152
      // Check ios-deploy is installed at meets version requirements.
153 154
      if (await hasIosDeploy) {
        messages.add(new ValidationMessage('ios-deploy ${await iosDeployVersionText}'));
155
      }
156
      if (!await _iosDeployIsInstalledAndMeetsVersionCheck) {
157
        brewStatus = ValidationType.partial;
158
        if (await hasIosDeploy) {
159
          messages.add(new ValidationMessage.error(
160
            'ios-deploy out of date ($iosDeployMinimumVersion is required). To upgrade:\n'
161
            '  brew upgrade ios-deploy'
162 163 164
          ));
        } else {
          messages.add(new ValidationMessage.error(
165
            'ios-deploy not installed. To install:\n'
166
            '  brew install ios-deploy'
167 168
          ));
        }
169
      }
170

171 172 173
      final CocoaPodsStatus cocoaPodsStatus = await cocoaPods.evaluateCocoaPodsInstallation;

      if (cocoaPodsStatus == CocoaPodsStatus.recommended) {
174 175
        if (await cocoaPods.isCocoaPodsInitialized) {
          messages.add(new ValidationMessage('CocoaPods version ${await cocoaPods.cocoaPodsVersionText}'));
176 177 178 179 180 181 182 183 184 185
        } else {
          brewStatus = ValidationType.partial;
          messages.add(new ValidationMessage.error(
            'CocoaPods installed but not initialized.\n'
            '$noCocoaPodsConsequence\n'
            'To initialize CocoaPods, run:\n'
            '  pod setup\n'
            'once to finalize CocoaPods\' installation.'
          ));
        }
186
      } else {
187
        brewStatus = ValidationType.partial;
188
        if (cocoaPodsStatus == CocoaPodsStatus.notInstalled) {
189
          messages.add(new ValidationMessage.error(
xster's avatar
xster committed
190 191 192 193
            'CocoaPods not installed.\n'
            '$noCocoaPodsConsequence\n'
            'To install:\n'
            '$cocoaPodsInstallInstructions'
194 195
          ));
        } else {
196 197
          messages.add(new ValidationMessage.hint(
            'CocoaPods out of date (${cocoaPods.cocoaPodsRecommendedVersion} is recommended).\n'
xster's avatar
xster committed
198 199 200
            '$noCocoaPodsConsequence\n'
            'To upgrade:\n'
            '$cocoaPodsUpgradeInstructions'
201 202 203
          ));
        }
      }
204
    } else {
205
      brewStatus = ValidationType.missing;
206 207
      messages.add(new ValidationMessage.error(
        'Brew not installed; use this to install tools for iOS device development.\n'
208
        'Download brew at https://brew.sh/.'
209 210 211 212
      ));
    }

    return new ValidationResult(
213
      <ValidationType>[xcodeStatus, brewStatus].reduce(_mergeValidationTypes),
214 215
      messages,
      statusInfo: xcodeVersionInfo
216
    );
217
  }
218 219 220 221

  ValidationType _mergeValidationTypes(ValidationType t1, ValidationType t2) {
    return t1 == t2 ? t1 : ValidationType.partial;
  }
222
}