android_workflow.dart 13.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// 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/process.dart';
11
import '../base/user_messages.dart';
12
import '../base/utils.dart';
13
import '../base/version.dart';
14
import '../convert.dart';
15
import '../doctor.dart';
16
import '../globals.dart' as globals;
17 18
import 'android_sdk.dart';

19
const int kAndroidSdkMinVersion = 28;
20
final Version kAndroidJavaMinVersion = Version(1, 8, 0);
21 22
final Version kAndroidSdkBuildToolsMinVersion = Version(28, 0, 3);

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

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

34 35 36
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.');
37

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

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

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

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

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

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

59 60 61 62 63 64 65 66 67 68 69
  /// Finds the semantic version anywhere in a text.
  static final RegExp _javaVersionPattern = RegExp(r'(\d+)(\.(\d+)(\.(\d+))?)?');

  /// `java -version` response is not only a number, but also includes other
  /// information eg. `openjdk version "1.7.0_212"`.
  /// This method extracts only the semantic version from from that response.
  static String _extractJavaVersion(String text) {
    final Match match = _javaVersionPattern.firstMatch(text ?? '');
    return text?.substring(match.start, match.end);
  }

70
  /// Returns false if we cannot determine the Java version or if the version
71
  /// is older that the minimum allowed version of 1.8.
72
  Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async {
73
    _task = 'Checking Java status';
74
    try {
75
      if (!globals.processManager.canRun(javaBinary)) {
76 77
        messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary)));
        return false;
78
      }
79
      String javaVersionText;
80
      try {
81 82
        globals.printTrace('java -version');
        final ProcessResult result = await globals.processManager.run(<String>[javaBinary, '-version']);
83
        if (result.exitCode == 0) {
84
          final List<String> versionLines = (result.stderr as String).split('\n');
85
          javaVersionText = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
86 87
        }
      } catch (error) {
88
        globals.printTrace(error.toString());
89
      }
90
      if (javaVersionText == null || javaVersionText.isEmpty) {
91 92 93 94
        // Could not determine the java version.
        messages.add(ValidationMessage.error(userMessages.androidUnknownJavaVersion));
        return false;
      }
95 96 97 98 99 100
      final Version javaVersion = Version.parse(_extractJavaVersion(javaVersionText));
      if (javaVersion < kAndroidJavaMinVersion) {
        messages.add(ValidationMessage.error(userMessages.androidJavaMinimumVersion(javaVersionText)));
        return false;
      }
      messages.add(ValidationMessage(userMessages.androidJavaVersion(javaVersionText)));
101 102 103
      return true;
    } finally {
      _task = null;
104 105 106
    }
  }

107
  @override
108
  Future<ValidationResult> validate() async {
109
    final List<ValidationMessage> messages = <ValidationMessage>[];
110 111

    if (androidSdk == null) {
112
      // No Android SDK found.
113 114
      if (globals.platform.environment.containsKey(kAndroidHome)) {
        final String androidHomeDir = globals.platform.environment[kAndroidHome];
115
        messages.add(ValidationMessage.error(userMessages.androidBadSdkDir(kAndroidHome, androidHomeDir)));
116
      } else {
117
        messages.add(ValidationMessage.error(userMessages.androidMissingSdkInstructions(kAndroidHome)));
118
      }
119
      return ValidationResult(ValidationType.missing, messages);
120
    }
121

122 123 124 125 126
    if (androidSdk.licensesAvailable && !androidSdk.platformToolsAvailable) {
      messages.add(ValidationMessage.hint(userMessages.androidSdkLicenseOnly(kAndroidHome)));
      return ValidationResult(ValidationType.partial, messages);
    }

127
    messages.add(ValidationMessage(userMessages.androidSdkLocation(androidSdk.directory)));
128

129
    messages.add(ValidationMessage(androidSdk.ndk == null
130 131
          ? userMessages.androidMissingNdk
          : userMessages.androidNdkLocation(androidSdk.ndk.directory)));
132

133 134
    String sdkVersionText;
    if (androidSdk.latestVersion != null) {
135 136 137 138 139 140
      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);
      }
141
      sdkVersionText = userMessages.androidStatusInfo(androidSdk.latestVersion.buildToolsVersionName);
142

143 144 145
      messages.add(ValidationMessage(userMessages.androidSdkPlatformToolsVersion(
        androidSdk.latestVersion.platformName,
        androidSdk.latestVersion.buildToolsVersionName)));
146 147
    } else {
      messages.add(ValidationMessage.error(userMessages.androidMissingSdkInstructions(kAndroidHome)));
148
    }
149

150 151
    if (globals.platform.environment.containsKey(kAndroidHome)) {
      final String androidHomeDir = globals.platform.environment[kAndroidHome];
152
      messages.add(ValidationMessage('$kAndroidHome = $androidHomeDir'));
153
    }
154 155
    if (globals.platform.environment.containsKey(kAndroidSdkRoot)) {
      final String androidSdkRoot = globals.platform.environment[kAndroidSdkRoot];
156 157
      messages.add(ValidationMessage('$kAndroidSdkRoot = $androidSdkRoot'));
    }
158

159
    final List<String> validationResult = androidSdk.validateSdkWellFormed();
160

161 162
    if (validationResult.isNotEmpty) {
      // Android SDK is not functional.
163
      messages.addAll(validationResult.map<ValidationMessage>((String message) {
164
        return ValidationMessage.error(message);
165
      }));
166
      messages.add(ValidationMessage(userMessages.androidSdkInstallHelp));
167
      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
168 169 170
    }

    // Now check for the JDK.
171
    final String javaBinary = AndroidSdk.findJavaBinary();
172
    if (javaBinary == null) {
173
      messages.add(ValidationMessage.error(userMessages.androidMissingJdk));
174
      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
175
    }
176
    messages.add(ValidationMessage(userMessages.androidJdkLocation(javaBinary)));
177 178

    // Check JDK version.
179
    if (! await _checkJavaVersion(javaBinary, messages)) {
180
      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
181 182
    }

183 184 185 186 187 188
    // Success.
    return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
  }
}

class AndroidLicenseValidator extends DoctorValidator {
189
  AndroidLicenseValidator() : super('Android license subvalidator',);
190

191 192 193
  @override
  String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...';

194 195 196 197 198 199 200 201 202 203 204
  @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);
    }

205
    final String sdkVersionText = userMessages.androidStatusInfo(androidSdk.latestVersion.buildToolsVersionName);
206

207 208 209
    // Check for licenses.
    switch (await licensesAccepted) {
      case LicensesAccepted.all:
210
        messages.add(ValidationMessage(userMessages.androidLicensesAll));
211 212
        break;
      case LicensesAccepted.some:
213
        messages.add(ValidationMessage.hint(userMessages.androidLicensesSome));
214
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
215
      case LicensesAccepted.none:
216
        messages.add(ValidationMessage.error(userMessages.androidLicensesNone));
217
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
218
      case LicensesAccepted.unknown:
219
        messages.add(ValidationMessage.error(userMessages.androidLicensesUnknown));
220
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
221
    }
222
    return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
223
  }
224

225 226 227 228 229
  Future<bool> _checkJavaVersionNoOutput() async {
    final String javaBinary = AndroidSdk.findJavaBinary();
    if (javaBinary == null) {
      return false;
    }
230
    if (!globals.processManager.canRun(javaBinary)) {
231 232 233 234
      return false;
    }
    String javaVersion;
    try {
235
      final ProcessResult result = await globals.processManager.run(<String>[javaBinary, '-version']);
236
      if (result.exitCode == 0) {
237
        final List<String> versionLines = (result.stderr as String).split('\n');
238 239 240
        javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
      }
    } catch (error) {
241
      globals.printTrace(error.toString());
242 243 244 245 246 247 248 249
    }
    if (javaVersion == null) {
      // Could not determine the java version.
      return false;
    }
    return true;
  }

250
  Future<LicensesAccepted> get licensesAccepted async {
251
    LicensesAccepted status;
252

253 254
    void _handleLine(String line) {
      if (licenseCounts.hasMatch(line)) {
255 256 257 258 259 260 261
        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)) {
262 263 264
        // 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.
265
        status = LicensesAccepted.none;
266 267
      } else if (licenseAccepted.hasMatch(line)) {
        status ??= LicensesAccepted.all;
268 269 270
      }
    }

271 272 273
    if (!_canRunSdkManager()) {
      return LicensesAccepted.unknown;
    }
274

275
    try {
276
      final Process process = await processUtils.start(
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
        <String>[androidSdk.sdkManagerPath, '--licenses'],
        environment: androidSdk.sdkManagerEnv,
      );
      process.stdin.write('n\n');
      // We expect logcat streams to occasionally contain invalid utf-8,
      // see: https://github.com/flutter/flutter/pull/8864.
      final Future<void> output = process.stdout
        .transform<String>(const Utf8Decoder(reportErrors: false))
        .transform<String>(const LineSplitter())
        .listen(_handleLine)
        .asFuture<void>(null);
      final Future<void> errors = process.stderr
        .transform<String>(const Utf8Decoder(reportErrors: false))
        .transform<String>(const LineSplitter())
        .listen(_handleLine)
        .asFuture<void>(null);
      await Future.wait<void>(<Future<void>>[output, errors]);
      return status ?? LicensesAccepted.unknown;
    } on ProcessException catch (e) {
296
      globals.printTrace('Failed to run Android sdk manager: $e');
297 298
      return LicensesAccepted.unknown;
    }
299 300
  }

301 302 303
  /// Run the Android SDK manager tool in order to accept SDK licenses.
  static Future<bool> runLicenseManager() async {
    if (androidSdk == null) {
304
      globals.printStatus(userMessages.androidSdkShort);
305 306 307
      return false;
    }

308 309 310
    if (!_canRunSdkManager()) {
      throwToolExit(userMessages.androidMissingSdkManager(androidSdk.sdkManagerPath));
    }
311

312
    final Version sdkManagerVersion = Version.parse(androidSdk.sdkManagerVersion);
313
    if (sdkManagerVersion == null || sdkManagerVersion.major < 26) {
314
      // SDK manager is found, but needs to be updated.
315
      throwToolExit(userMessages.androidSdkManagerOutdated(androidSdk.sdkManagerPath));
316
    }
317

318
    try {
319
      final Process process = await processUtils.start(
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
        <String>[androidSdk.sdkManagerPath, '--licenses'],
        environment: androidSdk.sdkManagerEnv,
      );

      // The real stdin will never finish streaming. Pipe until the child process
      // finishes.
      unawaited(process.stdin.addStream(stdin));
      // Wait for stdout and stderr to be fully processed, because process.exitCode
      // may complete first.
      await waitGroup<void>(<Future<void>>[
        stdout.addStream(process.stdout),
        stderr.addStream(process.stderr),
      ]);

      final int exitCode = await process.exitCode;
      return exitCode == 0;
    } on ProcessException catch (e) {
      throwToolExit(userMessages.androidCannotRunSdkManager(
          androidSdk.sdkManagerPath, e.toString()));
      return false;
    }
341
  }
342

343
  static bool _canRunSdkManager() {
344 345
    assert(androidSdk != null);
    final String sdkManagerPath = androidSdk.sdkManagerPath;
346
    return globals.processManager.canRun(sdkManagerPath);
347
  }
348
}