android_workflow.dart 16.3 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 7
import 'package:meta/meta.dart';
import 'package:process/process.dart';

8
import '../base/common.dart';
9
import '../base/context.dart';
10
import '../base/file_system.dart';
11
import '../base/io.dart';
12 13
import '../base/logger.dart';
import '../base/os.dart';
14
import '../base/platform.dart';
15
import '../base/process.dart';
16
import '../base/user_messages.dart';
17
import '../base/utils.dart';
18
import '../base/version.dart';
19
import '../convert.dart';
20
import '../doctor.dart';
21
import '../features.dart';
22
import '../globals.dart' as globals;
23
import 'android_sdk.dart';
24
import 'android_studio.dart';
25

26
const int kAndroidSdkMinVersion = 29;
27
final Version kAndroidJavaMinVersion = Version(1, 8, 0);
28 29
final Version kAndroidSdkBuildToolsMinVersion = Version(28, 0, 3);

30 31 32
AndroidWorkflow get androidWorkflow => context.get<AndroidWorkflow>();
AndroidValidator get androidValidator => context.get<AndroidValidator>();
AndroidLicenseValidator get androidLicenseValidator => context.get<AndroidLicenseValidator>();
33

34 35 36 37 38 39 40
enum LicensesAccepted {
  none,
  some,
  all,
  unknown,
}

41 42 43
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.');
44

45
class AndroidWorkflow implements Workflow {
46 47
  AndroidWorkflow({
    @required AndroidSdk androidSdk,
48 49 50
    @required FeatureFlags featureFlags,
  }) : _androidSdk = androidSdk,
       _featureFlags = featureFlags;
51 52

  final AndroidSdk _androidSdk;
53
  final FeatureFlags _featureFlags;
54

55
  @override
56
  bool get appliesToHostPlatform => _featureFlags.isAndroidEnabled;
57

58
  @override
59 60
  bool get canListDevices => _androidSdk != null
    && _androidSdk.adbPath != null;
61

62
  @override
63 64 65
  bool get canLaunchDevices => _androidSdk != null
    && _androidSdk.adbPath != null
    && _androidSdk.validateSdkWellFormed().isEmpty;
66

67
  @override
68 69 70
  bool get canListEmulators => _androidSdk != null
    && _androidSdk.adbPath != null
    && _androidSdk.emulatorPath != null;
71 72
}

73 74 75 76 77 78 79
/// 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.
80
class AndroidValidator extends DoctorValidator {
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
  AndroidValidator({
    @required AndroidSdk androidSdk,
    @required AndroidStudio androidStudio,
    @required FileSystem fileSystem,
    @required Logger logger,
    @required Platform platform,
    @required ProcessManager processManager,
    @required UserMessages userMessages,
  }) : _androidSdk = androidSdk,
       _androidStudio = androidStudio,
       _fileSystem = fileSystem,
       _logger = logger,
       _operatingSystemUtils = OperatingSystemUtils(
         fileSystem: fileSystem,
         logger: logger,
         platform: platform,
         processManager: processManager,
       ),
       _platform = platform,
       _processManager = processManager,
       _userMessages = userMessages,
       super('Android toolchain - develop for Android devices');

  final AndroidSdk _androidSdk;
  final AndroidStudio _androidStudio;
  final FileSystem _fileSystem;
  final Logger _logger;
  final OperatingSystemUtils _operatingSystemUtils;
  final Platform _platform;
  final ProcessManager _processManager;
  final UserMessages _userMessages;
112

113 114 115 116
  @override
  String get slowWarning => '${_task ?? 'This'} is taking a long time...';
  String _task;

117 118 119 120 121 122 123 124 125 126 127
  /// 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);
  }

128
  /// Returns false if we cannot determine the Java version or if the version
129
  /// is older that the minimum allowed version of 1.8.
130
  Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async {
131
    _task = 'Checking Java status';
132
    try {
133 134
      if (!_processManager.canRun(javaBinary)) {
        messages.add(ValidationMessage.error(_userMessages.androidCantRunJavaBinary(javaBinary)));
135
        return false;
136
      }
137
      String javaVersionText;
138
      try {
139 140
        _logger.printTrace('java -version');
        final ProcessResult result = await _processManager.run(<String>[javaBinary, '-version']);
141
        if (result.exitCode == 0) {
142
          final List<String> versionLines = (result.stderr as String).split('\n');
143
          javaVersionText = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
144
        }
145
      } on Exception catch (error) {
146
        _logger.printTrace(error.toString());
147
      }
148
      if (javaVersionText == null || javaVersionText.isEmpty) {
149
        // Could not determine the java version.
150
        messages.add(ValidationMessage.error(_userMessages.androidUnknownJavaVersion));
151 152
        return false;
      }
153 154
      final Version javaVersion = Version.parse(_extractJavaVersion(javaVersionText));
      if (javaVersion < kAndroidJavaMinVersion) {
155
        messages.add(ValidationMessage.error(_userMessages.androidJavaMinimumVersion(javaVersionText)));
156 157
        return false;
      }
158
      messages.add(ValidationMessage(_userMessages.androidJavaVersion(javaVersionText)));
159 160 161
      return true;
    } finally {
      _task = null;
162 163 164
    }
  }

165
  @override
166
  Future<ValidationResult> validate() async {
167
    final List<ValidationMessage> messages = <ValidationMessage>[];
168

169
    if (_androidSdk == null) {
170
      // No Android SDK found.
171 172 173
      if (_platform.environment.containsKey(kAndroidHome)) {
        final String androidHomeDir = _platform.environment[kAndroidHome];
        messages.add(ValidationMessage.error(_userMessages.androidBadSdkDir(kAndroidHome, androidHomeDir)));
174
      } else {
175 176
        // Instruct user to set [kAndroidSdkRoot] and not deprecated [kAndroidHome]
        // See https://github.com/flutter/flutter/issues/39301
177
        messages.add(ValidationMessage.error(_userMessages.androidMissingSdkInstructions(_platform)));
178
      }
179
      return ValidationResult(ValidationType.missing, messages);
180
    }
181

182 183
    if (_androidSdk.licensesAvailable && !_androidSdk.platformToolsAvailable) {
      messages.add(ValidationMessage.hint(_userMessages.androidSdkLicenseOnly(kAndroidHome)));
184 185 186
      return ValidationResult(ValidationType.partial, messages);
    }

187
    messages.add(ValidationMessage(_userMessages.androidSdkLocation(_androidSdk.directory)));
188

189
    String sdkVersionText;
190
    if (_androidSdk.latestVersion != null) {
191
      if (_androidSdk.latestVersion.sdkLevel < kAndroidSdkMinVersion || _androidSdk.latestVersion.buildToolsVersion < kAndroidSdkBuildToolsMinVersion) {
192
        messages.add(ValidationMessage.error(
193 194 195 196 197 198
          _userMessages.androidSdkBuildToolsOutdated(
            _androidSdk.sdkManagerPath,
            kAndroidSdkMinVersion,
            kAndroidSdkBuildToolsMinVersion.toString(),
            _platform,
          )),
199 200 201
        );
        return ValidationResult(ValidationType.missing, messages);
      }
202
      sdkVersionText = _userMessages.androidStatusInfo(_androidSdk.latestVersion.buildToolsVersionName);
203

204 205 206
      messages.add(ValidationMessage(_userMessages.androidSdkPlatformToolsVersion(
        _androidSdk.latestVersion.platformName,
        _androidSdk.latestVersion.buildToolsVersionName)));
207
    } else {
208
      messages.add(ValidationMessage.error(_userMessages.androidMissingSdkInstructions(_platform)));
209
    }
210

211 212
    if (_platform.environment.containsKey(kAndroidHome)) {
      final String androidHomeDir = _platform.environment[kAndroidHome];
213
      messages.add(ValidationMessage('$kAndroidHome = $androidHomeDir'));
214
    }
215 216
    if (_platform.environment.containsKey(kAndroidSdkRoot)) {
      final String androidSdkRoot = _platform.environment[kAndroidSdkRoot];
217 218
      messages.add(ValidationMessage('$kAndroidSdkRoot = $androidSdkRoot'));
    }
219

220
    final List<String> validationResult = _androidSdk.validateSdkWellFormed();
221

222 223
    if (validationResult.isNotEmpty) {
      // Android SDK is not functional.
224
      messages.addAll(validationResult.map<ValidationMessage>((String message) {
225
        return ValidationMessage.error(message);
226
      }));
227
      messages.add(ValidationMessage(_userMessages.androidSdkInstallHelp(_platform)));
228
      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
229 230 231
    }

    // Now check for the JDK.
232 233 234 235 236 237
    final String javaBinary = AndroidSdk.findJavaBinary(
      androidStudio: _androidStudio,
      fileSystem: _fileSystem,
      operatingSystemUtils: _operatingSystemUtils,
      platform: _platform,
    );
238
    if (javaBinary == null) {
239
      messages.add(ValidationMessage.error(_userMessages.androidMissingJdk));
240
      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
241
    }
242
    messages.add(ValidationMessage(_userMessages.androidJdkLocation(javaBinary)));
243 244

    // Check JDK version.
245
    if (! await _checkJavaVersion(javaBinary, messages)) {
246
      return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
247 248
    }

249 250 251 252 253
    // Success.
    return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
  }
}

254 255
/// A subvalidator that checks if the licenses within the detected Android
/// SDK have been accepted.
256
class AndroidLicenseValidator extends DoctorValidator {
257
  AndroidLicenseValidator() : super('Android license subvalidator',);
258

259 260 261
  @override
  String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...';

262 263 264 265 266
  @override
  Future<ValidationResult> validate() async {
    final List<ValidationMessage> messages = <ValidationMessage>[];

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

273
    final String sdkVersionText = userMessages.androidStatusInfo(globals.androidSdk.latestVersion.buildToolsVersionName);
274

275 276 277
    // Check for licenses.
    switch (await licensesAccepted) {
      case LicensesAccepted.all:
278
        messages.add(ValidationMessage(userMessages.androidLicensesAll));
279 280
        break;
      case LicensesAccepted.some:
281
        messages.add(ValidationMessage.hint(userMessages.androidLicensesSome));
282
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
283
      case LicensesAccepted.none:
284
        messages.add(ValidationMessage.error(userMessages.androidLicensesNone));
285
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
286
      case LicensesAccepted.unknown:
287
        messages.add(ValidationMessage.error(userMessages.androidLicensesUnknown(globals.platform)));
288
        return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
289
    }
290
    return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
291
  }
292

293
  Future<bool> _checkJavaVersionNoOutput() async {
294 295 296 297 298 299
    final String javaBinary = AndroidSdk.findJavaBinary(
      androidStudio: globals.androidStudio,
      fileSystem: globals.fs,
      operatingSystemUtils: globals.os,
      platform: globals.platform,
    );
300 301 302
    if (javaBinary == null) {
      return false;
    }
303
    if (!globals.processManager.canRun(javaBinary)) {
304 305 306 307
      return false;
    }
    String javaVersion;
    try {
308
      final ProcessResult result = await globals.processManager.run(<String>[javaBinary, '-version']);
309
      if (result.exitCode == 0) {
310
        final List<String> versionLines = (result.stderr as String).split('\n');
311 312
        javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
      }
313
    } on Exception catch (error) {
314
      globals.printTrace(error.toString());
315 316 317 318 319 320 321 322
    }
    if (javaVersion == null) {
      // Could not determine the java version.
      return false;
    }
    return true;
  }

323
  Future<LicensesAccepted> get licensesAccepted async {
324
    LicensesAccepted status;
325

326 327
    void _handleLine(String line) {
      if (licenseCounts.hasMatch(line)) {
328 329 330 331 332 333 334
        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)) {
335 336 337
        // 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.
338
        status = LicensesAccepted.none;
339 340
      } else if (licenseAccepted.hasMatch(line)) {
        status ??= LicensesAccepted.all;
341 342 343
      }
    }

344 345 346
    if (!_canRunSdkManager()) {
      return LicensesAccepted.unknown;
    }
347

348
    try {
349
      final Process process = await processUtils.start(
350 351
        <String>[globals.androidSdk.sdkManagerPath, '--licenses'],
        environment: globals.androidSdk.sdkManagerEnv,
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
      );
      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) {
369
      globals.printTrace('Failed to run Android sdk manager: $e');
370 371
      return LicensesAccepted.unknown;
    }
372 373
  }

374 375
  /// Run the Android SDK manager tool in order to accept SDK licenses.
  static Future<bool> runLicenseManager() async {
376
    if (globals.androidSdk == null) {
377
      globals.printStatus(userMessages.androidSdkShort);
378 379 380
      return false;
    }

381
    if (!_canRunSdkManager()) {
382
      throwToolExit(userMessages.androidMissingSdkManager(globals.androidSdk.sdkManagerPath, globals.platform));
383
    }
384

385
    try {
386
      final Process process = await processUtils.start(
387 388
        <String>[globals.androidSdk.sdkManagerPath, '--licenses'],
        environment: globals.androidSdk.sdkManagerEnv,
389 390 391 392
      );

      // The real stdin will never finish streaming. Pipe until the child process
      // finishes.
393 394 395 396 397 398 399 400 401
      unawaited(process.stdin.addStream(globals.stdio.stdin)
        // If the process exits unexpectedly with an error, that will be
        // handled by the caller.
        .catchError((dynamic err, StackTrace stack) {
          globals.printTrace('Echoing stdin to the licenses subprocess failed:');
          globals.printTrace('$err\n$stack');
        }
      ));

402 403
      // Wait for stdout and stderr to be fully processed, because process.exitCode
      // may complete first.
404 405 406 407 408
      try {
        await waitGroup<void>(<Future<void>>[
          globals.stdio.addStdoutStream(process.stdout),
          globals.stdio.addStderrStream(process.stderr),
        ]);
409
      } on Exception catch (err, stack) {
410 411 412
        globals.printTrace('Echoing stdout or stderr from the license subprocess failed:');
        globals.printTrace('$err\n$stack');
      }
413 414 415 416 417

      final int exitCode = await process.exitCode;
      return exitCode == 0;
    } on ProcessException catch (e) {
      throwToolExit(userMessages.androidCannotRunSdkManager(
418
        globals.androidSdk.sdkManagerPath,
419
        e.toString(),
420
        globals.platform,
421
      ));
422 423
      return false;
    }
424
  }
425

426
  static bool _canRunSdkManager() {
427 428
    assert(globals.androidSdk != null);
    final String sdkManagerPath = globals.androidSdk.sdkManagerPath;
429
    return globals.processManager.canRun(sdkManagerPath);
430
  }
431
}