ios_workflow.dart 8.47 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 17
IOSWorkflow get iosWorkflow => context.putIfAbsent(IOSWorkflow, () => new IOSWorkflow());

18 19
class IOSWorkflow extends DoctorValidator implements Workflow {
  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;
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
  Future<bool> get hasIDeviceInstaller => exitsHappyAsync(<String>['ideviceinstaller', '-h']);
34

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

37
  String get iosDeployMinimumVersion => '1.9.2';
38

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

41 42
  bool get hasHomebrew => os.which('brew') != null;

43
  bool get hasPythonSixModule => kPythonSix.isInstalled;
44

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

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

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

    if (xcode.isInstalled) {
67
      xcodeStatus = ValidationType.installed;
68

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

71
      xcodeVersionInfo = xcode.versionText;
72 73
      if (xcodeVersionInfo.contains(','))
        xcodeVersionInfo = xcodeVersionInfo.substring(0, xcodeVersionInfo.indexOf(','));
74
      messages.add(new ValidationMessage(xcode.versionText));
75 76

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

      if (!xcode.eulaSigned) {
85
        xcodeStatus = ValidationType.partial;
86
        messages.add(new ValidationMessage.error(
87
          'Xcode end user license agreement not signed; open Xcode or run the command \'sudo xcodebuild -license\'.'
88 89
        ));
      }
90 91 92 93
      if ((await macDevMode).contains('disabled')) {
        xcodeStatus = ValidationType.partial;
        messages.add(new ValidationMessage.error(
          'Your Mac needs to enabled for developer mode before using Xcode for the first time.\n'
94
          'Run \'sudo DevToolsSecurity -enable\' to enable developer mode.'
95 96 97
        ));
      }

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

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

124
    // brew installed
125
    if (hasHomebrew) {
126
      brewStatus = ValidationType.installed;
127

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

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

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

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

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