ios_workflow.dart 8.61 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

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

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

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

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

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

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

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

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

40
  String get iosDeployMinimumVersion => '1.9.2';
41

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

44 45
  bool get hasHomebrew => os.which('brew') != null;

46
  bool get hasPythonSixModule => kPythonSix.isInstalled;
47

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

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

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

    if (xcode.isInstalled) {
70
      xcodeStatus = ValidationType.installed;
71

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

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

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

      if (!xcode.eulaSigned) {
88
        xcodeStatus = ValidationType.partial;
89
        messages.add(new ValidationMessage.error(
90
          'Xcode end user license agreement not signed; open Xcode or run the command \'sudo xcodebuild -license\'.'
91 92
        ));
      }
93 94 95 96 97 98 99
      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.'
        ));
      }
100

101
    } else {
102
      xcodeStatus = ValidationType.missing;
103
      if (xcode.xcodeSelectPath == null || xcode.xcodeSelectPath.isEmpty) {
104 105 106 107 108 109 110
        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'
111
            'Download at: https://developer.apple.com/xcode/download/\n'
112 113 114
            'Or install Xcode via the App Store.\n'
            'Once installed, run:\n'
            '  sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer'
115 116
        ));
      }
117 118
    }

119 120 121 122 123
    // Python dependencies installed
    if (hasPythonSixModule) {
      pythonStatus = ValidationType.installed;
    } else {
      pythonStatus = ValidationType.missing;
124
      messages.add(new ValidationMessage.error(kPythonSix.errorMessage));
125 126
    }

127
    // brew installed
128
    if (hasHomebrew) {
129
      brewStatus = ValidationType.installed;
130

131
      if (!iMobileDevice.isInstalled) {
132 133
        brewStatus = ValidationType.partial;
        messages.add(new ValidationMessage.error(
134 135 136 137 138 139 140
            '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(
141 142 143
            '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'
144 145 146
            '  brew uninstall --ignore-dependencies libimobiledevice\n'
            '  brew install --HEAD libimobiledevice\n'
            '  brew install ideviceinstaller'
147
        ));
148
      } else if (!await hasIDeviceInstaller) {
149
        brewStatus = ValidationType.partial;
150
        messages.add(new ValidationMessage.error(
151
          'ideviceinstaller is not installed; this is used to discover connected iOS devices.\n'
152
          'To install, run:\n'
153 154
          '  brew install --HEAD libimobiledevice\n'
          '  brew install ideviceinstaller'
155 156 157
        ));
      }

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

177 178 179
      final CocoaPodsStatus cocoaPodsStatus = await cocoaPods.evaluateCocoaPodsInstallation;

      if (cocoaPodsStatus == CocoaPodsStatus.recommended) {
180 181
        if (await cocoaPods.isCocoaPodsInitialized) {
          messages.add(new ValidationMessage('CocoaPods version ${await cocoaPods.cocoaPodsVersionText}'));
182 183 184 185 186 187 188 189 190 191
        } 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.'
          ));
        }
192
      } else {
193
        brewStatus = ValidationType.partial;
194
        if (cocoaPodsStatus == CocoaPodsStatus.notInstalled) {
195
          messages.add(new ValidationMessage.error(
xster's avatar
xster committed
196 197 198 199
            'CocoaPods not installed.\n'
            '$noCocoaPodsConsequence\n'
            'To install:\n'
            '$cocoaPodsInstallInstructions'
200 201
          ));
        } else {
202 203
          messages.add(new ValidationMessage.hint(
            'CocoaPods out of date (${cocoaPods.cocoaPodsRecommendedVersion} is recommended).\n'
xster's avatar
xster committed
204 205 206
            '$noCocoaPodsConsequence\n'
            'To upgrade:\n'
            '$cocoaPodsUpgradeInstructions'
207 208 209
          ));
        }
      }
210
    } else {
211
      brewStatus = ValidationType.missing;
212 213
      messages.add(new ValidationMessage.error(
        'Brew not installed; use this to install tools for iOS device development.\n'
214
        'Download brew at https://brew.sh/.'
215 216 217 218
      ));
    }

    return new ValidationResult(
219
      <ValidationType>[xcodeStatus, pythonStatus, brewStatus].reduce(_mergeValidationTypes),
220 221
      messages,
      statusInfo: xcodeVersionInfo
222
    );
223
  }
224 225 226 227

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