android_workflow.dart 12.1 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/common.dart';
8
import '../base/context.dart';
9
import '../base/io.dart';
10
import '../base/platform.dart';
11
import '../base/process.dart';
12
import '../base/process_manager.dart';
13
import '../base/user_messages.dart';
14
import '../base/utils.dart';
15
import '../base/version.dart';
16
import '../convert.dart';
17
import '../doctor.dart';
18
import '../globals.dart';
19 20
import 'android_sdk.dart';

21 22 23
const int kAndroidSdkMinVersion = 28;
final Version kAndroidSdkBuildToolsMinVersion = Version(28, 0, 3);

24 25 26
AndroidWorkflow get androidWorkflow => context.get<AndroidWorkflow>();
AndroidValidator get androidValidator => context.get<AndroidValidator>();
AndroidLicenseValidator get androidLicenseValidator => context.get<AndroidLicenseValidator>();
27

28 29 30 31 32 33 34
enum LicensesAccepted {
  none,
  some,
  all,
  unknown,
}

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

39
class AndroidWorkflow implements Workflow {
40
  @override
41 42
  bool get appliesToHostPlatform => true;

43
  @override
44 45
  bool get canListDevices => getAdbPath(androidSdk) != null;

46
  @override
47
  bool get canLaunchDevices => androidSdk != null && androidSdk.validateSdkWellFormed().isEmpty;
48

49
  @override
50
  bool get canListEmulators => getEmulatorPath(androidSdk) != null;
51 52 53
}

class AndroidValidator extends DoctorValidator {
54
  AndroidValidator() : super('Android toolchain - develop for Android devices',);
55

56 57 58 59
  @override
  String get slowWarning => '${_task ?? 'This'} is taking a long time...';
  String _task;

60 61
  /// Returns false if we cannot determine the Java version or if the version
  /// is not compatible.
62
  Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async {
63
    _task = 'Checking Java status';
64
    try {
65 66 67
      if (!processManager.canRun(javaBinary)) {
        messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary)));
        return false;
68
      }
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
      String javaVersion;
      try {
        printTrace('java -version');
        final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']);
        if (result.exitCode == 0) {
          final List<String> versionLines = result.stderr.split('\n');
          javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
        }
      } catch (error) {
        printTrace(error.toString());
      }
      if (javaVersion == null) {
        // Could not determine the java version.
        messages.add(ValidationMessage.error(userMessages.androidUnknownJavaVersion));
        return false;
      }
      messages.add(ValidationMessage(userMessages.androidJavaVersion(javaVersion)));
      // TODO(johnmccutchan): Validate version.
      return true;
    } finally {
      _task = null;
90 91 92
    }
  }

93
  @override
94
  Future<ValidationResult> validate() async {
95
    final List<ValidationMessage> messages = <ValidationMessage>[];
96 97

    if (androidSdk == null) {
98
      // No Android SDK found.
99
      if (platform.environment.containsKey(kAndroidHome)) {
100
        final String androidHomeDir = platform.environment[kAndroidHome];
101
        messages.add(ValidationMessage.error(userMessages.androidBadSdkDir(kAndroidHome, androidHomeDir)));
102
      } else {
103
        messages.add(ValidationMessage.error(userMessages.androidMissingSdkInstructions(kAndroidHome)));
104
      }
105
      return ValidationResult(ValidationType.missing, messages);
106
    }
107

108 109 110 111 112
    if (androidSdk.licensesAvailable && !androidSdk.platformToolsAvailable) {
      messages.add(ValidationMessage.hint(userMessages.androidSdkLicenseOnly(kAndroidHome)));
      return ValidationResult(ValidationType.partial, messages);
    }

113
    messages.add(ValidationMessage(userMessages.androidSdkLocation(androidSdk.directory)));
114

115
    messages.add(ValidationMessage(androidSdk.ndk == null
116 117
          ? userMessages.androidMissingNdk
          : userMessages.androidNdkLocation(androidSdk.ndk.directory)));
118

119 120
    String sdkVersionText;
    if (androidSdk.latestVersion != null) {
121 122 123 124 125 126
      if (androidSdk.latestVersion.sdkLevel < 28 || androidSdk.latestVersion.buildToolsVersion < kAndroidSdkBuildToolsMinVersion) {
        messages.add(ValidationMessage.error(
          userMessages.androidSdkBuildToolsOutdated(androidSdk.sdkManagerPath, kAndroidSdkMinVersion, kAndroidSdkBuildToolsMinVersion.toString())),
        );
        return ValidationResult(ValidationType.missing, messages);
      }
127
      sdkVersionText = userMessages.androidStatusInfo(androidSdk.latestVersion.buildToolsVersionName);
128

129 130 131
      messages.add(ValidationMessage(userMessages.androidSdkPlatformToolsVersion(
        androidSdk.latestVersion.platformName,
        androidSdk.latestVersion.buildToolsVersionName)));
132 133
    } else {
      messages.add(ValidationMessage.error(userMessages.androidMissingSdkInstructions(kAndroidHome)));
134
    }
135

136 137
    if (platform.environment.containsKey(kAndroidHome)) {
      final String androidHomeDir = platform.environment[kAndroidHome];
138
      messages.add(ValidationMessage('$kAndroidHome = $androidHomeDir'));
139
    }
140 141 142 143
    if (platform.environment.containsKey(kAndroidSdkRoot)) {
      final String androidSdkRoot = platform.environment[kAndroidSdkRoot];
      messages.add(ValidationMessage('$kAndroidSdkRoot = $androidSdkRoot'));
    }
144

145
    final List<String> validationResult = androidSdk.validateSdkWellFormed();
146

147 148
    if (validationResult.isNotEmpty) {
      // Android SDK is not functional.
149
      messages.addAll(validationResult.map<ValidationMessage>((String message) {
150
        return ValidationMessage.error(message);
151
      }));
152
      messages.add(ValidationMessage(userMessages.androidSdkInstallHelp));
153
      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
154 155 156
    }

    // Now check for the JDK.
157
    final String javaBinary = AndroidSdk.findJavaBinary();
158
    if (javaBinary == null) {
159
      messages.add(ValidationMessage.error(userMessages.androidMissingJdk));
160
      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
161
    }
162
    messages.add(ValidationMessage(userMessages.androidJdkLocation(javaBinary)));
163 164

    // Check JDK version.
165
    if (! await _checkJavaVersion(javaBinary, messages)) {
166
      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
167 168
    }

169 170 171 172 173 174
    // Success.
    return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
  }
}

class AndroidLicenseValidator extends DoctorValidator {
175
  AndroidLicenseValidator() : super('Android license subvalidator',);
176

177 178 179
  @override
  String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...';

180 181 182 183 184 185 186 187 188 189 190
  @override
  Future<ValidationResult> validate() async {
    final List<ValidationMessage> messages = <ValidationMessage>[];

    // Match pre-existing early termination behavior
    if (androidSdk == null || androidSdk.latestVersion == null ||
        androidSdk.validateSdkWellFormed().isNotEmpty ||
        ! await _checkJavaVersionNoOutput()) {
      return ValidationResult(ValidationType.missing, messages);
    }

191
    final String sdkVersionText = userMessages.androidStatusInfo(androidSdk.latestVersion.buildToolsVersionName);
192

193 194 195
    // Check for licenses.
    switch (await licensesAccepted) {
      case LicensesAccepted.all:
196
        messages.add(ValidationMessage(userMessages.androidLicensesAll));
197 198
        break;
      case LicensesAccepted.some:
199
        messages.add(ValidationMessage.hint(userMessages.androidLicensesSome));
200
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
201
      case LicensesAccepted.none:
202
        messages.add(ValidationMessage.error(userMessages.androidLicensesNone));
203
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
204
      case LicensesAccepted.unknown:
205
        messages.add(ValidationMessage.error(userMessages.androidLicensesUnknown));
206
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
207
    }
208
    return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
209
  }
210

211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
  Future<bool> _checkJavaVersionNoOutput() async {
    final String javaBinary = AndroidSdk.findJavaBinary();
    if (javaBinary == null) {
      return false;
    }
    if (!processManager.canRun(javaBinary)) {
      return false;
    }
    String javaVersion;
    try {
      final ProcessResult result = await processManager.run(<String>[javaBinary, '-version']);
      if (result.exitCode == 0) {
        final List<String> versionLines = result.stderr.split('\n');
        javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
      }
    } catch (error) {
      printTrace(error.toString());
    }
    if (javaVersion == null) {
      // Could not determine the java version.
      return false;
    }
    return true;
  }

236
  Future<LicensesAccepted> get licensesAccepted async {
237
    LicensesAccepted status;
238

239 240
    void _handleLine(String line) {
      if (licenseCounts.hasMatch(line)) {
241 242 243 244 245 246 247
        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)) {
248 249 250
        // The licenseNotAccepted pattern is trying to match the same line as
        // licenseCounts, but is more general. In case the format changes, a
        // more general match may keep doctor mostly working.
251
        status = LicensesAccepted.none;
252 253
      } else if (licenseAccepted.hasMatch(line)) {
        status ??= LicensesAccepted.all;
254 255 256
      }
    }

257 258 259
    if (!_canRunSdkManager()) {
      return LicensesAccepted.unknown;
    }
260 261 262 263 264

    final Process process = await runCommand(
      <String>[androidSdk.sdkManagerPath, '--licenses'],
      environment: androidSdk.sdkManagerEnv,
    );
265
    process.stdin.write('n\n');
266 267
    // We expect logcat streams to occasionally contain invalid utf-8,
    // see: https://github.com/flutter/flutter/pull/8864.
268
    final Future<void> output = process.stdout
269
      .transform<String>(const Utf8Decoder(reportErrors: false))
270
      .transform<String>(const LineSplitter())
271
      .listen(_handleLine)
272
      .asFuture<void>(null);
273
    final Future<void> errors = process.stderr
274
      .transform<String>(const Utf8Decoder(reportErrors: false))
275
      .transform<String>(const LineSplitter())
276
      .listen(_handleLine)
277
      .asFuture<void>(null);
278
    await Future.wait<void>(<Future<void>>[output, errors]);
279
    return status ?? LicensesAccepted.unknown;
280 281
  }

282 283 284
  /// Run the Android SDK manager tool in order to accept SDK licenses.
  static Future<bool> runLicenseManager() async {
    if (androidSdk == null) {
285
      printStatus(userMessages.androidSdkShort);
286 287 288
      return false;
    }

289 290 291
    if (!_canRunSdkManager()) {
      throwToolExit(userMessages.androidMissingSdkManager(androidSdk.sdkManagerPath));
    }
292

293
    final Version sdkManagerVersion = Version.parse(androidSdk.sdkManagerVersion);
294
    if (sdkManagerVersion == null || sdkManagerVersion.major < 26) {
295
      // SDK manager is found, but needs to be updated.
296
      throwToolExit(userMessages.androidSdkManagerOutdated(androidSdk.sdkManagerPath));
297
    }
298

299
    final Process process = await runCommand(
300
      <String>[androidSdk.sdkManagerPath, '--licenses'],
301
      environment: androidSdk.sdkManagerEnv,
302
    );
303

304 305
    // The real stdin will never finish streaming. Pipe until the child process
    // finishes.
306
    unawaited(process.stdin.addStream(stdin));
307 308
    // Wait for stdout and stderr to be fully processed, because process.exitCode
    // may complete first.
309
    await waitGroup<void>(<Future<void>>[
310 311 312
      stdout.addStream(process.stdout),
      stderr.addStream(process.stderr),
    ]);
313 314 315 316

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

318
  static bool _canRunSdkManager() {
319 320
    assert(androidSdk != null);
    final String sdkManagerPath = androidSdk.sdkManagerPath;
321
    return processManager.canRun(sdkManagerPath);
322
  }
323
}