ios_workflow.dart 7.79 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/io.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 'mac.dart';
14

15
Xcode get xcode => Xcode.instance;
16 17 18

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

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

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

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

  bool get hasIDeviceId => exitsHappy(<String>['idevice_id', '-h']);
33

34 35
  bool get hasIDeviceInstaller => exitsHappy(<String>['ideviceinstaller', '-h']);

36
  bool get hasIosDeploy => exitsHappy(<String>['ios-deploy', '--version']);
37 38 39 40 41

  String get iosDeployMinimumVersion => '1.9.0';

  String get iosDeployVersionText => runSync(<String>['ios-deploy', '--version']).replaceAll('\n', '');

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

44
  bool get hasPythonSixModule => kPythonSix.isInstalled;
45

46 47 48 49 50 51
  bool get hasCocoaPods => exitsHappy(<String>['pod', '--version']);

  String get cocoaPodsMinimumVersion => '1.0.0';

  String get cocoaPodsVersionText => runSync(<String>['pod', '--version']).trim();

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

63
  bool get cocoaPodsInstalledAndMeetsVersionCheck {
64 65 66 67 68 69 70 71 72 73
    if (!hasCocoaPods)
      return false;
    try {
      final Version installedVersion = new Version.parse(cocoaPodsVersionText);
      return installedVersion >= new Version.parse(cocoaPodsMinimumVersion);
    } on FormatException {
      return false;
    }
  }

74
  @override
75
  Future<ValidationResult> validate() async {
76
    final List<ValidationMessage> messages = <ValidationMessage>[];
77
    ValidationType xcodeStatus = ValidationType.missing;
78
    ValidationType pythonStatus = ValidationType.missing;
79
    ValidationType brewStatus = ValidationType.missing;
80 81 82
    String xcodeVersionInfo;

    if (xcode.isInstalled) {
83
      xcodeStatus = ValidationType.installed;
84

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

87 88 89 90 91 92
      xcodeVersionInfo = xcode.xcodeVersionText;
      if (xcodeVersionInfo.contains(','))
        xcodeVersionInfo = xcodeVersionInfo.substring(0, xcodeVersionInfo.indexOf(','));
      messages.add(new ValidationMessage(xcode.xcodeVersionText));

      if (!xcode.isInstalledAndMeetsVersionCheck) {
93
        xcodeStatus = ValidationType.partial;
94
        messages.add(new ValidationMessage.error(
95
          'Flutter requires a minimum Xcode version of $kXcodeRequiredVersionMajor.$kXcodeRequiredVersionMinor.0.\n'
96 97 98 99 100
          'Download the latest version or update via the Mac App Store.'
        ));
      }

      if (!xcode.eulaSigned) {
101
        xcodeStatus = ValidationType.partial;
102
        messages.add(new ValidationMessage.error(
103
          'Xcode end user license agreement not signed; open Xcode or run the command \'sudo xcodebuild -license\'.'
104 105 106
        ));
      }
    } else {
107
      xcodeStatus = ValidationType.missing;
108
      if (xcode.xcodeSelectPath == null || xcode.xcodeSelectPath.isEmpty) {
109 110 111 112 113 114 115
        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'
116
            'Download at: https://developer.apple.com/xcode/download/\n'
117
            'Once installed, run \'sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer\'.'
118 119
        ));
      }
120 121
    }

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

130
    // brew installed
131
    if (hasHomebrew) {
132
      brewStatus = ValidationType.installed;
133

134
      if (!hasIDeviceInstaller) {
135
        brewStatus = ValidationType.partial;
136 137
        messages.add(new ValidationMessage.error(
          'ideviceinstaller not available; this is used to discover connected iOS devices.\n'
138
          'To install, run:\n'
139 140 141
          '  brew update\n'
          '  brew install --HEAD libimobiledevice\n'
          '  brew install ideviceinstaller'
142 143 144
        ));
      }

145 146 147 148 149
      // Check ios-deploy is installed at meets version requirements.
      if (hasIosDeploy) {
        messages.add(new ValidationMessage('ios-deploy $iosDeployVersionText'));
      }
      if (!hasIDeviceId || !_iosDeployIsInstalledAndMeetsVersionCheck) {
150
        brewStatus = ValidationType.partial;
151 152
        if (hasIosDeploy) {
          messages.add(new ValidationMessage.error(
153
            'ios-deploy out of date ($iosDeployMinimumVersion is required). To upgrade:\n'
154 155
            '  brew update\n'
            '  brew upgrade ios-deploy'
156 157 158
          ));
        } else {
          messages.add(new ValidationMessage.error(
159
            'ios-deploy not installed. To install:\n'
160 161
            '  brew update\n'
            '  brew install ios-deploy'
162 163
          ));
        }
164 165 166
      } else {
        // Check for compatibility between libimobiledevice and Xcode.
        // TODO(cbracken) remove this check once libimobiledevice > 1.2.0 is released.
167
        final ProcessResult result = (await runAsync(<String>['idevice_id', '-l'])).processResult;
168
        if (result.exitCode == 0 && result.stdout.isNotEmpty && !exitsHappy(<String>['ideviceName'])) {
169
          brewStatus = ValidationType.partial;
170
          messages.add(new ValidationMessage.error(
171
            'libimobiledevice is incompatible with the installed Xcode version. To update, run:\n'
172 173 174
            '  brew update\n'
            '  brew uninstall --ignore-dependencies libimobiledevice\n'
            '  brew install --HEAD libimobiledevice'
175 176
          ));
        }
177
      }
178
      if (cocoaPodsInstalledAndMeetsVersionCheck) {
179 180
        messages.add(new ValidationMessage('CocoaPods version $cocoaPodsVersionText'));
      } else {
181
        brewStatus = ValidationType.partial;
182 183 184
        if (!hasCocoaPods) {
          messages.add(new ValidationMessage.error(
            'CocoaPods not installed. To install:\n'
185 186 187
            '  brew update\n'
            '  brew install cocoapods\n'
            '  pod setup'
188 189 190 191
          ));
        } else {
          messages.add(new ValidationMessage.error(
            'CocoaPods out of date ($cocoaPodsMinimumVersion is required). To upgrade:\n'
192 193 194
            '  brew update\n'
            '  brew upgrade cocoapods\n'
            '  pod setup'
195 196 197
          ));
        }
      }
198
    } else {
199
      brewStatus = ValidationType.missing;
200 201 202 203 204 205 206
      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(
207
      <ValidationType>[xcodeStatus, pythonStatus, brewStatus].reduce(_mergeValidationTypes),
208 209
      messages,
      statusInfo: xcodeVersionInfo
210
    );
211
  }
212 213 214 215

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