android_workflow.dart 9.42 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
import 'dart:convert';
7

8
import '../base/common.dart';
9
import '../base/context.dart';
10
import '../base/io.dart';
11
import '../base/platform.dart';
12
import '../base/process.dart';
13
import '../base/process_manager.dart';
14
import '../base/utils.dart';
15
import '../base/version.dart';
16
import '../doctor.dart';
17
import '../globals.dart';
18 19
import 'android_sdk.dart';

20
AndroidWorkflow get androidWorkflow => context[AndroidWorkflow];
21

22 23 24 25 26 27 28 29 30 31 32
enum LicensesAccepted {
  none,
  some,
  all,
  unknown,
}

final RegExp licenseCounts = new RegExp(r'(\d+) of (\d+) SDK package licenses? not accepted.');
final RegExp licenseNotAccepted = new RegExp(r'licenses? not accepted', caseSensitive: false);
final RegExp licenseAccepted = new RegExp(r'All SDK package licenses accepted.');

33 34
class AndroidWorkflow extends DoctorValidator implements Workflow {
  AndroidWorkflow() : super('Android toolchain - develop for Android devices');
35

36
  @override
37 38
  bool get appliesToHostPlatform => true;

39
  @override
40 41
  bool get canListDevices => getAdbPath(androidSdk) != null;

42
  @override
43
  bool get canLaunchDevices => androidSdk != null && androidSdk.validateSdkWellFormed().isEmpty;
44

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
  static const String _kJdkDownload = 'https://www.oracle.com/technetwork/java/javase/downloads/';

  /// Returns false if we cannot determine the Java version or if the version
  /// is not compatible.
  bool _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) {
    if (!processManager.canRun(javaBinary)) {
      messages.add(new ValidationMessage.error('Cannot execute $javaBinary to determine the version'));
      return false;
    }
    String javaVersion;
    try {
      printTrace('java -version');
      final ProcessResult result = processManager.runSync(<String>[javaBinary, '-version']);
      if (result.exitCode == 0) {
        final List<String> versionLines = result.stderr.split('\n');
        javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
      }
    } catch (_) { /* ignore */ }
    if (javaVersion == null) {
      // Could not determine the java version.
      messages.add(new ValidationMessage.error('Could not determine java version'));
      return false;
    }
68
    messages.add(new ValidationMessage('Java version $javaVersion'));
69 70 71 72
    // TODO(johnmccutchan): Validate version.
    return true;
  }

73
  @override
74
  Future<ValidationResult> validate() async {
75
    final List<ValidationMessage> messages = <ValidationMessage>[];
76 77

    if (androidSdk == null) {
78
      // No Android SDK found.
79
      if (platform.environment.containsKey(kAndroidHome)) {
80
        final String androidHomeDir = platform.environment[kAndroidHome];
81 82
        messages.add(new ValidationMessage.error(
          '$kAndroidHome = $androidHomeDir\n'
83
          'but Android SDK not found at this location.'
84 85 86
        ));
      } else {
        messages.add(new ValidationMessage.error(
87
          'Unable to locate Android SDK.\n'
88
          'Install Android Studio from: https://developer.android.com/studio/index.html\n'
89
          'On first launch it will assist you in installing the Android SDK components.\n'
90 91
          '(or visit https://flutter.io/setup/#android-setup for detailed instructions).\n'
          'If Android SDK has been installed to a custom location, set \$$kAndroidHome to that location.'
92 93
        ));
      }
94

95 96
      return new ValidationResult(ValidationType.missing, messages);
    }
97

98
    messages.add(new ValidationMessage('Android SDK at ${androidSdk.directory}'));
99

100
    messages.add(new ValidationMessage(androidSdk.ndkDirectory == null
101
          ? 'Android NDK location not configured (optional; useful for native profiling support)'
102 103
          : 'Android NDK at ${androidSdk.ndkDirectory}'));

104 105 106
    String sdkVersionText;
    if (androidSdk.latestVersion != null) {
      sdkVersionText = 'Android SDK ${androidSdk.latestVersion.buildToolsVersionName}';
107

108
      messages.add(new ValidationMessage(
109
        'Platform ${androidSdk.latestVersion.platformName}, '
110 111 112
        'build-tools ${androidSdk.latestVersion.buildToolsVersionName}'
      ));
    }
113

114 115 116 117
    if (platform.environment.containsKey(kAndroidHome)) {
      final String androidHomeDir = platform.environment[kAndroidHome];
      messages.add(new ValidationMessage('$kAndroidHome = $androidHomeDir'));
    }
118

119
    final List<String> validationResult = androidSdk.validateSdkWellFormed();
120

121 122 123 124 125 126
    if (validationResult.isNotEmpty) {
      // Android SDK is not functional.
      messages.addAll(validationResult.map((String message) {
        return new ValidationMessage.error(message);
      }));
      messages.add(new ValidationMessage(
127 128
          'Try re-installing or updating your Android SDK,\n'
          'visit https://flutter.io/setup/#android-setup for detailed instructions.'));
129 130 131 132
      return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
    }

    // Now check for the JDK.
133
    final String javaBinary = AndroidSdk.findJavaBinary();
134 135 136 137 138 139 140 141 142 143 144 145
    if (javaBinary == null) {
      messages.add(new ValidationMessage.error(
          'No Java Development Kit (JDK) found; You must have the environment '
          'variable JAVA_HOME set and the java binary in your PATH. '
          'You can download the JDK from $_kJdkDownload.'));
      return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
    }
    messages.add(new ValidationMessage('Java binary at: $javaBinary'));

    // Check JDK version.
    if (!_checkJavaVersion(javaBinary, messages)) {
      return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
146 147
    }

148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
    // Check for licenses.
    switch (await licensesAccepted) {
      case LicensesAccepted.all:
        messages.add(new ValidationMessage('All Android licenses accepted.'));
        break;
      case LicensesAccepted.some:
        messages.add(new ValidationMessage.hint('Some Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses'));
        return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
      case LicensesAccepted.none:
        messages.add(new ValidationMessage.error('Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses'));
        return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
      case LicensesAccepted.unknown:
        messages.add(new ValidationMessage.error('Android license status unknown.'));
        return new ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
    }

164 165
    // Success.
    return new ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
166
  }
167

168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
  Future<LicensesAccepted> get licensesAccepted async {
    LicensesAccepted status = LicensesAccepted.unknown;

    void _onLine(String line) {
      if (licenseAccepted.hasMatch(line)) {
        status = LicensesAccepted.all;
      } else if (licenseCounts.hasMatch(line)) {
        final Match match = licenseCounts.firstMatch(line);
        if (match.group(1) != match.group(2)) {
          status = LicensesAccepted.some;
        } else {
          status = LicensesAccepted.none;
        }
      } else if (licenseNotAccepted.hasMatch(line)) {
        // In case the format changes, a more general match will keep doctor
        // mostly working.
        status = LicensesAccepted.none;
      }
    }

    final Process process = await runCommand(<String>[androidSdk.sdkManagerPath, '--licenses'], environment: androidSdk.sdkManagerEnv);
    process.stdin.write('n\n');
    final Future<void> output = process.stdout.transform(const Utf8Decoder(allowMalformed: true)).transform(const LineSplitter()).listen(_onLine).asFuture<void>(null);
    final Future<void> errors = process.stderr.transform(const Utf8Decoder(allowMalformed: true)).transform(const LineSplitter()).listen(_onLine).asFuture<void>(null);
    try {
      await Future.wait<void>(<Future<void>>[output, errors]).timeout(const Duration(seconds: 30));
    } catch (TimeoutException) {
      printTrace('Intentionally killing ${androidSdk.sdkManagerPath}');
      processManager.killPid(process.pid);
    }
    return status;
  }

201 202 203 204 205 206 207
  /// Run the Android SDK manager tool in order to accept SDK licenses.
  static Future<bool> runLicenseManager() async {
    if (androidSdk == null) {
      printStatus('Unable to locate Android SDK.');
      return false;
    }

208 209 210 211 212 213 214 215
    if (!processManager.canRun(androidSdk.sdkManagerPath))
      throwToolExit(
        'Android sdkmanager tool not found.\n'
        'Try re-installing or updating your Android SDK,\n'
        'visit https://flutter.io/setup/#android-setup for detailed instructions.'
      );

    final Version sdkManagerVersion = new Version.parse(androidSdk.sdkManagerVersion);
216
    if (sdkManagerVersion == null || sdkManagerVersion.major < 26)
217 218 219 220 221 222
      // SDK manager is found, but needs to be updated.
      throwToolExit(
        'A newer version of the Android SDK is required. To update, run:\n'
        '${androidSdk.sdkManagerPath} --update\n'
      );

223
    final Process process = await runCommand(
224
      <String>[androidSdk.sdkManagerPath, '--licenses'],
225
      environment: androidSdk.sdkManagerEnv,
226
    );
227

228 229
    process.stdin.addStream(stdin);
    await waitGroup<Null>(<Future<Null>>[
230 231 232
      stdout.addStream(process.stdout),
      stderr.addStream(process.stderr),
    ]);
233 234 235 236

    final int exitCode = await process.exitCode;
    return exitCode == 0;
  }
237
}