ios_workflow.dart 8.02 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 72 73 74 75 76
      xcodeVersionInfo = xcode.xcodeVersionText;
      if (xcodeVersionInfo.contains(','))
        xcodeVersionInfo = xcodeVersionInfo.substring(0, xcodeVersionInfo.indexOf(','));
      messages.add(new ValidationMessage(xcode.xcodeVersionText));

      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 (!await iMobileDevice.isWorking) {
129 130
        brewStatus = ValidationType.partial;
        messages.add(new ValidationMessage.error(
131
            'libimobiledevice and ideviceinstaller are not installed or require updating. To update, run:\n'
132 133 134
            '  brew uninstall --ignore-dependencies libimobiledevice\n'
            '  brew install --HEAD libimobiledevice\n'
            '  brew install ideviceinstaller'
135
        ));
136
      } else if (!await hasIDeviceInstaller) {
137
        brewStatus = ValidationType.partial;
138 139
        messages.add(new ValidationMessage.error(
          'ideviceinstaller not available; this is used to discover connected iOS devices.\n'
140
          'To install, run:\n'
141 142
          '  brew install --HEAD libimobiledevice\n'
          '  brew install ideviceinstaller'
143 144 145
        ));
      }

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

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

    return new ValidationResult(
205
      <ValidationType>[xcodeStatus, pythonStatus, brewStatus].reduce(_mergeValidationTypes),
206 207
      messages,
      statusInfo: xcodeVersionInfo
208
    );
209
  }
210 211 212 213

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