android_workflow.dart 16.6 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 6
import 'dart:async';

7
import 'package:process/process.dart';
8

9
import '../base/common.dart';
10
import '../base/context.dart';
11
import '../base/io.dart';
12
import '../base/logger.dart';
13
import '../base/platform.dart';
14
import '../base/user_messages.dart' hide userMessages;
15
import '../base/version.dart';
16
import '../convert.dart';
17
import '../doctor_validator.dart';
18
import '../features.dart';
19
import 'android_sdk.dart';
20
import 'java.dart';
21

22
const int kAndroidSdkMinVersion = 29;
23
final Version kAndroidJavaMinVersion = Version(1, 8, 0);
24 25
final Version kAndroidSdkBuildToolsMinVersion = Version(28, 0, 3);

26 27 28
AndroidWorkflow? get androidWorkflow => context.get<AndroidWorkflow>();
AndroidValidator? get androidValidator => context.get<AndroidValidator>();
AndroidLicenseValidator? get androidLicenseValidator => context.get<AndroidLicenseValidator>();
29

30 31 32 33 34 35 36
enum LicensesAccepted {
  none,
  some,
  all,
  unknown,
}

37 38 39
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.');
40

41
class AndroidWorkflow implements Workflow {
42
  AndroidWorkflow({
43 44
    required AndroidSdk? androidSdk,
    required FeatureFlags featureFlags,
45
  }) : _androidSdk = androidSdk,
46
       _featureFlags = featureFlags;
47

48
  final AndroidSdk? _androidSdk;
49
  final FeatureFlags _featureFlags;
50

51
  @override
52
  bool get appliesToHostPlatform => _featureFlags.isAndroidEnabled;
53

54
  @override
55
  bool get canListDevices => appliesToHostPlatform && _androidSdk != null
56
    && _androidSdk.adbPath != null;
57

58
  @override
59
  bool get canLaunchDevices => appliesToHostPlatform && _androidSdk != null
60 61
    && _androidSdk.adbPath != null
    && _androidSdk.validateSdkWellFormed().isEmpty;
62

63
  @override
64
  bool get canListEmulators => canListDevices && _androidSdk?.emulatorPath != null;
65 66
}

67 68 69 70 71 72 73
/// A validator that checks if the Android SDK and Java SDK are available and
/// installed correctly.
///
/// Android development requires the Android SDK, and at least one Java SDK. While
/// newer Java compilers can be used to compile the Java application code, the SDK
/// tools themselves required JDK 1.8. This older JDK is normally bundled with
/// Android Studio.
74
class AndroidValidator extends DoctorValidator {
75
  AndroidValidator({
76
    required Java? java,
77 78 79 80
    required AndroidSdk? androidSdk,
    required Logger logger,
    required Platform platform,
    required UserMessages userMessages,
81 82
  }) : _java = java,
       _androidSdk = androidSdk,
83 84 85 86 87
       _logger = logger,
       _platform = platform,
       _userMessages = userMessages,
       super('Android toolchain - develop for Android devices');

88
  final Java? _java;
89
  final AndroidSdk? _androidSdk;
90 91 92
  final Logger _logger;
  final Platform _platform;
  final UserMessages _userMessages;
93

94 95
  @override
  String get slowWarning => '${_task ?? 'This'} is taking a long time...';
96
  String? _task;
97

98
  /// Returns false if we cannot determine the Java version or if the version
99
  /// is older that the minimum allowed version of 1.8.
100
  Future<bool> _checkJavaVersion(List<ValidationMessage> messages) async {
101
    _task = 'Checking Java status';
102
    try {
103 104
      if (_java?.binaryPath == null) {
        messages.add(ValidationMessage.error(_userMessages.androidMissingJdk));
105
        return false;
106
      }
107
      messages.add(ValidationMessage(_userMessages.androidJdkLocation(_java!.binaryPath)));
108 109
      if (!_java.canRun()) {
        messages.add(ValidationMessage.error(_userMessages.androidCantRunJavaBinary(_java.binaryPath)));
110 111 112
        return false;
      }
      Version? javaVersion;
113
      try {
114
        javaVersion = _java.version;
115
      } on Exception catch (error) {
116
        _logger.printTrace(error.toString());
117
      }
118
      if (javaVersion == null) {
119
        // Could not determine the java version.
120
        messages.add(ValidationMessage.error(_userMessages.androidUnknownJavaVersion));
121 122
        return false;
      }
123
      if (javaVersion < kAndroidJavaMinVersion) {
124
        messages.add(ValidationMessage.error(_userMessages.androidJavaMinimumVersion(javaVersion.toString())));
125 126
        return false;
      }
127
      messages.add(ValidationMessage(_userMessages.androidJavaVersion(javaVersion.toString())));
128 129 130
      return true;
    } finally {
      _task = null;
131 132 133
    }
  }

134
  @override
135
  Future<ValidationResult> validate() async {
136
    final List<ValidationMessage> messages = <ValidationMessage>[];
137 138
    final AndroidSdk? androidSdk = _androidSdk;
    if (androidSdk == null) {
139
      // No Android SDK found.
140
      if (_platform.environment.containsKey(kAndroidHome)) {
141
        final String androidHomeDir = _platform.environment[kAndroidHome]!;
142
        messages.add(ValidationMessage.error(_userMessages.androidBadSdkDir(kAndroidHome, androidHomeDir)));
143
      } else {
144 145
        // Instruct user to set [kAndroidSdkRoot] and not deprecated [kAndroidHome]
        // See https://github.com/flutter/flutter/issues/39301
146
        messages.add(ValidationMessage.error(_userMessages.androidMissingSdkInstructions(_platform)));
147
      }
148
      return ValidationResult(ValidationType.missing, messages);
149
    }
150 151 152

    messages.add(ValidationMessage(_userMessages.androidSdkLocation(androidSdk.directory.path)));

153
    _task = 'Validating Android SDK command line tools are available';
154
    if (!androidSdk.cmdlineToolsAvailable) {
155
      messages.add(ValidationMessage.error(_userMessages.androidMissingCmdTools));
156 157
      return ValidationResult(ValidationType.missing, messages);
    }
158

159
    _task = 'Validating Android SDK licenses';
160
    if (androidSdk.licensesAvailable && !androidSdk.platformToolsAvailable) {
161
      messages.add(ValidationMessage.hint(_userMessages.androidSdkLicenseOnly(kAndroidHome)));
162 163 164
      return ValidationResult(ValidationType.partial, messages);
    }

165 166 167 168
    String? sdkVersionText;
    final AndroidSdkVersion? androidSdkLatestVersion = androidSdk.latestVersion;
    if (androidSdkLatestVersion != null) {
      if (androidSdkLatestVersion.sdkLevel < kAndroidSdkMinVersion || androidSdkLatestVersion.buildToolsVersion < kAndroidSdkBuildToolsMinVersion) {
169
        messages.add(ValidationMessage.error(
170 171 172 173 174
          _userMessages.androidSdkBuildToolsOutdated(
            kAndroidSdkMinVersion,
            kAndroidSdkBuildToolsMinVersion.toString(),
            _platform,
          )),
175 176 177
        );
        return ValidationResult(ValidationType.missing, messages);
      }
178
      sdkVersionText = _userMessages.androidStatusInfo(androidSdkLatestVersion.buildToolsVersionName);
179

180
      messages.add(ValidationMessage(_userMessages.androidSdkPlatformToolsVersion(
181 182
        androidSdkLatestVersion.platformName,
        androidSdkLatestVersion.buildToolsVersionName)));
183
    } else {
184
      messages.add(ValidationMessage.error(_userMessages.androidMissingSdkInstructions(_platform)));
185
    }
186

187
    if (_platform.environment.containsKey(kAndroidHome)) {
188
      final String androidHomeDir = _platform.environment[kAndroidHome]!;
189
      messages.add(ValidationMessage('$kAndroidHome = $androidHomeDir'));
190
    }
191
    if (_platform.environment.containsKey(kAndroidSdkRoot)) {
192
      final String androidSdkRoot = _platform.environment[kAndroidSdkRoot]!;
193 194
      messages.add(ValidationMessage('$kAndroidSdkRoot = $androidSdkRoot'));
    }
195

196
    _task = 'Validating Android SDK';
197
    final List<String> validationResult = androidSdk.validateSdkWellFormed();
198

199 200
    if (validationResult.isNotEmpty) {
      // Android SDK is not functional.
201
      messages.addAll(validationResult.map<ValidationMessage>((String message) {
202
        return ValidationMessage.error(message);
203
      }));
204
      messages.add(ValidationMessage(_userMessages.androidSdkInstallHelp(_platform)));
205
      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
206 207
    }

208
    _task = 'Finding Java binary';
209 210

    // Check JDK version.
211
    if (!await _checkJavaVersion(messages)) {
212
      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
213 214
    }

215
    // Success.
216
    return ValidationResult(ValidationType.success, messages, statusInfo: sdkVersionText);
217 218 219
  }
}

220 221
/// A subvalidator that checks if the licenses within the detected Android
/// SDK have been accepted.
222
class AndroidLicenseValidator extends DoctorValidator {
223
  AndroidLicenseValidator({
224
    required Java? java,
225
    required AndroidSdk? androidSdk,
226 227 228 229 230
    required Platform platform,
    required ProcessManager processManager,
    required Logger logger,
    required Stdio stdio,
    required UserMessages userMessages,
231 232
  }) : _java = java,
       _androidSdk = androidSdk,
233 234 235 236 237 238 239
       _platform = platform,
       _processManager = processManager,
       _logger = logger,
       _stdio = stdio,
       _userMessages = userMessages,
       super('Android license subvalidator');

240
  final Java? _java;
241
  final AndroidSdk? _androidSdk;
242 243 244 245 246
  final Stdio _stdio;
  final Platform _platform;
  final ProcessManager _processManager;
  final Logger _logger;
  final UserMessages _userMessages;
247

248 249 250
  @override
  String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...';

251 252 253 254 255
  @override
  Future<ValidationResult> validate() async {
    final List<ValidationMessage> messages = <ValidationMessage>[];

    // Match pre-existing early termination behavior
256 257
    if (_androidSdk == null || _androidSdk.latestVersion == null ||
        _androidSdk.validateSdkWellFormed().isNotEmpty ||
258 259 260 261
        ! await _checkJavaVersionNoOutput()) {
      return ValidationResult(ValidationType.missing, messages);
    }

262
    final String sdkVersionText = _userMessages.androidStatusInfo(_androidSdk.latestVersion!.buildToolsVersionName);
263

264 265 266
    // Check for licenses.
    switch (await licensesAccepted) {
      case LicensesAccepted.all:
267
        messages.add(ValidationMessage(_userMessages.androidLicensesAll));
268
      case LicensesAccepted.some:
269
        messages.add(ValidationMessage.hint(_userMessages.androidLicensesSome));
270
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
271
      case LicensesAccepted.none:
272
        messages.add(ValidationMessage.error(_userMessages.androidLicensesNone));
273
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
274
      case LicensesAccepted.unknown:
275
        messages.add(ValidationMessage.error(_userMessages.androidLicensesUnknown(_platform)));
276
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
277
    }
278
    return ValidationResult(ValidationType.success, messages, statusInfo: sdkVersionText);
279
  }
280

281
  Future<bool> _checkJavaVersionNoOutput() async {
282 283
    final String? javaBinary = _java?.binaryPath;

284 285 286
    if (javaBinary == null) {
      return false;
    }
287
    if (!_processManager.canRun(javaBinary)) {
288 289
      return false;
    }
290
    String? javaVersion;
291
    try {
292
      final ProcessResult result = await _processManager.run(<String>[javaBinary, '-version']);
293
      if (result.exitCode == 0) {
294
        final List<String> versionLines = (result.stderr as String).split('\n');
295 296
        javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
      }
297
    } on Exception catch (error) {
298
      _logger.printTrace(error.toString());
299 300 301 302 303 304 305 306
    }
    if (javaVersion == null) {
      // Could not determine the java version.
      return false;
    }
    return true;
  }

307
  Future<LicensesAccepted> get licensesAccepted async {
308
    LicensesAccepted? status;
309

310
    void handleLine(String line) {
311
      if (licenseCounts.hasMatch(line)) {
312 313
        final Match? match = licenseCounts.firstMatch(line);
        if (match?.group(1) != match?.group(2)) {
314 315 316 317 318
          status = LicensesAccepted.some;
        } else {
          status = LicensesAccepted.none;
        }
      } else if (licenseNotAccepted.hasMatch(line)) {
319 320 321
        // 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.
322
        status = LicensesAccepted.none;
323 324
      } else if (licenseAccepted.hasMatch(line)) {
        status ??= LicensesAccepted.all;
325 326 327
      }
    }

328 329 330
    if (!_canRunSdkManager()) {
      return LicensesAccepted.unknown;
    }
331

332
    try {
333
      final Process process = await _processManager.start(
334
        <String>[_androidSdk!.sdkManagerPath!, '--licenses'],
335
        environment: _java?.environment,
336 337 338 339 340 341 342
      );
      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())
343
        .listen(handleLine)
344
        .asFuture<void>();
345 346 347
      final Future<void> errors = process.stderr
        .transform<String>(const Utf8Decoder(reportErrors: false))
        .transform<String>(const LineSplitter())
348
        .listen(handleLine)
349
        .asFuture<void>();
350 351 352
      await Future.wait<void>(<Future<void>>[output, errors]);
      return status ?? LicensesAccepted.unknown;
    } on ProcessException catch (e) {
353
      _logger.printTrace('Failed to run Android sdk manager: $e');
354 355
      return LicensesAccepted.unknown;
    }
356 357
  }

358
  /// Run the Android SDK manager tool in order to accept SDK licenses.
359 360 361
  Future<bool> runLicenseManager() async {
    if (_androidSdk == null) {
      _logger.printStatus(_userMessages.androidSdkShort);
362 363 364
      return false;
    }

365
    if (!_canRunSdkManager()) {
366 367 368 369
      throwToolExit(
        'Android sdkmanager not found. Update to the latest Android SDK and ensure that '
        'the cmdline-tools are installed to resolve this.'
      );
370
    }
371

372
    try {
373
      final Process process = await _processManager.start(
374
        <String>[_androidSdk.sdkManagerPath!, '--licenses'],
375
        environment: _java?.environment,
376 377 378 379
      );

      // The real stdin will never finish streaming. Pipe until the child process
      // finishes.
380
      unawaited(process.stdin.addStream(_stdio.stdin)
381 382
        // If the process exits unexpectedly with an error, that will be
        // handled by the caller.
383 384 385
        .then(
          (Object? socket) => socket,
          onError: (dynamic err, StackTrace stack) {
386 387
            _logger.printTrace('Echoing stdin to the licenses subprocess failed:');
            _logger.printTrace('$err\n$stack');
388 389 390
          },
        ),
      );
391

392
      final List<String> stderrLines = <String>[];
393 394
      // Wait for stdout and stderr to be fully processed, because process.exitCode
      // may complete first.
395
      try {
396
        await Future.wait<void>(<Future<void>>[
397
          _stdio.addStdoutStream(process.stdout),
398 399 400 401
          process.stderr.forEach((List<int> event) {
            _stdio.stderr.add(event);
            stderrLines.add(utf8.decode(event));
          }),
402
        ]);
403
      } on Exception catch (err, stack) {
404 405
        _logger.printTrace('Echoing stdout or stderr from the license subprocess failed:');
        _logger.printTrace('$err\n$stack');
406
      }
407 408

      final int exitCode = await process.exitCode;
409
      if (exitCode != 0) {
410
        throwToolExit(_messageForSdkManagerError(stderrLines, exitCode));
411 412
      }
      return true;
413
    } on ProcessException catch (e) {
414
      throwToolExit(_userMessages.androidCannotRunSdkManager(
415
        _androidSdk.sdkManagerPath ?? '',
416
        e.toString(),
417
        _platform,
418
      ));
419
    }
420
  }
421

422
  bool _canRunSdkManager() {
423
    final String? sdkManagerPath = _androidSdk?.sdkManagerPath;
424 425 426
    if (sdkManagerPath == null) {
      return false;
    }
427
    return _processManager.canRun(sdkManagerPath);
428
  }
429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454

  String _messageForSdkManagerError(
    List<String> androidSdkStderr,
    int exitCode,
  ) {
    final String sdkManagerPath = _androidSdk!.sdkManagerPath!;

    final bool failedDueToJdkIncompatibility = androidSdkStderr.join().contains(
      RegExp(r'java\.lang\.UnsupportedClassVersionError.*SdkManagerCli '
        r'has been compiled by a more recent version of the Java Runtime'));

    if (failedDueToJdkIncompatibility) {
      return 'Android sdkmanager tool was found, but failed to run ($sdkManagerPath): "exited code $exitCode".\n'
        'It appears the version of the Java binary used (${_java!.binaryPath}) is '
        'too out-of-date and is incompatible with the Android sdkmanager tool.\n'
        'If the Java binary came bundled with Android Studio, consider updating '
        'your installation of Android studio. Alternatively, you can uninstall '
        'the Android SDK command-line tools and install an earlier version. ';
    }

    return _userMessages.androidCannotRunSdkManager(
      sdkManagerPath,
      'exited code $exitCode',
      _platform,
    );
  }
455
}