blob: 20244ddb07d2b689c0fd7c1a57996c12dc7fedff [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:process/process.dart';
import '../base/common.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/user_messages.dart' hide userMessages;
import '../base/version.dart';
import '../convert.dart';
import '../doctor_validator.dart';
import '../features.dart';
import 'android_sdk.dart';
import 'android_studio.dart';
const int kAndroidSdkMinVersion = 29;
final Version kAndroidJavaMinVersion = Version(1, 8, 0);
final Version kAndroidSdkBuildToolsMinVersion = Version(28, 0, 3);
AndroidWorkflow? get androidWorkflow => context.get<AndroidWorkflow>();
AndroidValidator? get androidValidator => context.get<AndroidValidator>();
AndroidLicenseValidator? get androidLicenseValidator => context.get<AndroidLicenseValidator>();
enum LicensesAccepted {
none,
some,
all,
unknown,
}
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.');
class AndroidWorkflow implements Workflow {
AndroidWorkflow({
required AndroidSdk? androidSdk,
required FeatureFlags featureFlags,
required OperatingSystemUtils operatingSystemUtils,
}) : _androidSdk = androidSdk,
_featureFlags = featureFlags,
_operatingSystemUtils = operatingSystemUtils;
final AndroidSdk? _androidSdk;
final FeatureFlags _featureFlags;
final OperatingSystemUtils _operatingSystemUtils;
@override
bool get appliesToHostPlatform => _featureFlags.isAndroidEnabled
// Android Studio is not currently supported on Linux Arm64 Hosts.
&& _operatingSystemUtils.hostPlatform != HostPlatform.linux_arm64;
@override
bool get canListDevices => appliesToHostPlatform && _androidSdk != null
&& _androidSdk?.adbPath != null;
@override
bool get canLaunchDevices => appliesToHostPlatform && _androidSdk != null
&& _androidSdk?.adbPath != null
&& (_androidSdk?.validateSdkWellFormed().isEmpty ?? false);
@override
bool get canListEmulators => canListDevices && _androidSdk?.emulatorPath != null;
}
/// 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.
class AndroidValidator extends DoctorValidator {
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;
@override
String get slowWarning => '${_task ?? 'This'} is taking a long time...';
String? _task;
/// 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 that response.
static String? _extractJavaVersion(String? text) {
if (text == null || text.isEmpty) {
return null;
}
final Match? match = _javaVersionPattern.firstMatch(text);
if (match == null) {
return null;
}
return text.substring(match.start, match.end);
}
/// Returns false if we cannot determine the Java version or if the version
/// is older that the minimum allowed version of 1.8.
Future<bool> _checkJavaVersion(String javaBinary, List<ValidationMessage> messages) async {
_task = 'Checking Java status';
try {
if (!_processManager.canRun(javaBinary)) {
messages.add(ValidationMessage.error(_userMessages.androidCantRunJavaBinary(javaBinary)));
return false;
}
String? javaVersionText;
try {
_logger.printTrace('java -version');
final ProcessResult result = await _processManager.run(<String>[javaBinary, '-version']);
if (result.exitCode == 0) {
final List<String> versionLines = (result.stderr as String).split('\n');
javaVersionText = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
}
} on Exception catch (error) {
_logger.printTrace(error.toString());
}
final Version? javaVersion = Version.parse(_extractJavaVersion(javaVersionText));
if (javaVersionText == null || javaVersionText.isEmpty || javaVersion == null) {
// Could not determine the java version.
messages.add(ValidationMessage.error(_userMessages.androidUnknownJavaVersion));
return false;
}
if (javaVersion < kAndroidJavaMinVersion) {
messages.add(ValidationMessage.error(_userMessages.androidJavaMinimumVersion(javaVersionText)));
return false;
}
messages.add(ValidationMessage(_userMessages.androidJavaVersion(javaVersionText)));
return true;
} finally {
_task = null;
}
}
@override
Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[];
final AndroidSdk? androidSdk = _androidSdk;
if (androidSdk == null) {
// No Android SDK found.
if (_platform.environment.containsKey(kAndroidHome)) {
final String androidHomeDir = _platform.environment[kAndroidHome]!;
messages.add(ValidationMessage.error(_userMessages.androidBadSdkDir(kAndroidHome, androidHomeDir)));
} else {
// Instruct user to set [kAndroidSdkRoot] and not deprecated [kAndroidHome]
// See https://github.com/flutter/flutter/issues/39301
messages.add(ValidationMessage.error(_userMessages.androidMissingSdkInstructions(_platform)));
}
return ValidationResult(ValidationType.missing, messages);
}
messages.add(ValidationMessage(_userMessages.androidSdkLocation(androidSdk.directory.path)));
_task = 'Validating Android SDK command line tools are available';
if (!androidSdk.cmdlineToolsAvailable) {
messages.add(ValidationMessage.error(_userMessages.androidMissingCmdTools));
return ValidationResult(ValidationType.missing, messages);
}
_task = 'Validating Android SDK licenses';
if (androidSdk.licensesAvailable && !androidSdk.platformToolsAvailable) {
messages.add(ValidationMessage.hint(_userMessages.androidSdkLicenseOnly(kAndroidHome)));
return ValidationResult(ValidationType.partial, messages);
}
String? sdkVersionText;
final AndroidSdkVersion? androidSdkLatestVersion = androidSdk.latestVersion;
if (androidSdkLatestVersion != null) {
if (androidSdkLatestVersion.sdkLevel < kAndroidSdkMinVersion || androidSdkLatestVersion.buildToolsVersion < kAndroidSdkBuildToolsMinVersion) {
messages.add(ValidationMessage.error(
_userMessages.androidSdkBuildToolsOutdated(
kAndroidSdkMinVersion,
kAndroidSdkBuildToolsMinVersion.toString(),
_platform,
)),
);
return ValidationResult(ValidationType.missing, messages);
}
sdkVersionText = _userMessages.androidStatusInfo(androidSdkLatestVersion.buildToolsVersionName);
messages.add(ValidationMessage(_userMessages.androidSdkPlatformToolsVersion(
androidSdkLatestVersion.platformName,
androidSdkLatestVersion.buildToolsVersionName)));
} else {
messages.add(ValidationMessage.error(_userMessages.androidMissingSdkInstructions(_platform)));
}
if (_platform.environment.containsKey(kAndroidHome)) {
final String androidHomeDir = _platform.environment[kAndroidHome]!;
messages.add(ValidationMessage('$kAndroidHome = $androidHomeDir'));
}
if (_platform.environment.containsKey(kAndroidSdkRoot)) {
final String androidSdkRoot = _platform.environment[kAndroidSdkRoot]!;
messages.add(ValidationMessage('$kAndroidSdkRoot = $androidSdkRoot'));
}
_task = 'Validating Android SDK';
final List<String> validationResult = androidSdk.validateSdkWellFormed();
if (validationResult.isNotEmpty) {
// Android SDK is not functional.
messages.addAll(validationResult.map<ValidationMessage>((String message) {
return ValidationMessage.error(message);
}));
messages.add(ValidationMessage(_userMessages.androidSdkInstallHelp(_platform)));
return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
}
_task = 'Finding Java binary';
// Now check for the JDK.
final String? javaBinary = AndroidSdk.findJavaBinary(
androidStudio: _androidStudio,
fileSystem: _fileSystem,
operatingSystemUtils: _operatingSystemUtils,
platform: _platform,
);
if (javaBinary == null) {
messages.add(ValidationMessage.error(_userMessages.androidMissingJdk));
return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
}
messages.add(ValidationMessage(_userMessages.androidJdkLocation(javaBinary)));
// Check JDK version.
if (!await _checkJavaVersion(javaBinary, messages)) {
return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
}
// Success.
return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
}
}
/// A subvalidator that checks if the licenses within the detected Android
/// SDK have been accepted.
class AndroidLicenseValidator extends DoctorValidator {
AndroidLicenseValidator({
required AndroidSdk? androidSdk,
required Platform platform,
required OperatingSystemUtils operatingSystemUtils,
required FileSystem fileSystem,
required ProcessManager processManager,
required Logger logger,
required AndroidStudio? androidStudio,
required Stdio stdio,
required UserMessages userMessages,
}) : _androidSdk = androidSdk,
_platform = platform,
_operatingSystemUtils = operatingSystemUtils,
_fileSystem = fileSystem,
_processManager = processManager,
_logger = logger,
_androidStudio = androidStudio,
_stdio = stdio,
_userMessages = userMessages,
super('Android license subvalidator');
final AndroidSdk? _androidSdk;
final AndroidStudio? _androidStudio;
final Stdio _stdio;
final OperatingSystemUtils _operatingSystemUtils;
final Platform _platform;
final FileSystem _fileSystem;
final ProcessManager _processManager;
final Logger _logger;
final UserMessages _userMessages;
@override
String get slowWarning => 'Checking Android licenses is taking an unexpectedly long time...';
@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);
}
final String sdkVersionText = _userMessages.androidStatusInfo(_androidSdk!.latestVersion!.buildToolsVersionName);
// Check for licenses.
switch (await licensesAccepted) {
case LicensesAccepted.all:
messages.add(ValidationMessage(_userMessages.androidLicensesAll));
break;
case LicensesAccepted.some:
messages.add(ValidationMessage.hint(_userMessages.androidLicensesSome));
return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
case LicensesAccepted.none:
messages.add(ValidationMessage.error(_userMessages.androidLicensesNone));
return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
case LicensesAccepted.unknown:
messages.add(ValidationMessage.error(_userMessages.androidLicensesUnknown(_platform)));
return ValidationResult(ValidationType.partial, messages, statusInfo: sdkVersionText);
}
return ValidationResult(ValidationType.installed, messages, statusInfo: sdkVersionText);
}
Future<bool> _checkJavaVersionNoOutput() async {
final String? javaBinary = AndroidSdk.findJavaBinary(
androidStudio: _androidStudio,
fileSystem: _fileSystem,
operatingSystemUtils: _operatingSystemUtils,
platform: _platform,
);
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 as String).split('\n');
javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
}
} on Exception catch (error) {
_logger.printTrace(error.toString());
}
if (javaVersion == null) {
// Could not determine the java version.
return false;
}
return true;
}
Future<LicensesAccepted> get licensesAccepted async {
LicensesAccepted? status;
void handleLine(String line) {
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)) {
// 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.
status = LicensesAccepted.none;
} else if (licenseAccepted.hasMatch(line)) {
status ??= LicensesAccepted.all;
}
}
if (!_canRunSdkManager()) {
return LicensesAccepted.unknown;
}
try {
final Process process = await _processManager.start(
<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>();
final Future<void> errors = process.stderr
.transform<String>(const Utf8Decoder(reportErrors: false))
.transform<String>(const LineSplitter())
.listen(handleLine)
.asFuture<void>();
await Future.wait<void>(<Future<void>>[output, errors]);
return status ?? LicensesAccepted.unknown;
} on ProcessException catch (e) {
_logger.printTrace('Failed to run Android sdk manager: $e');
return LicensesAccepted.unknown;
}
}
/// Run the Android SDK manager tool in order to accept SDK licenses.
Future<bool> runLicenseManager() async {
if (_androidSdk == null) {
_logger.printStatus(_userMessages.androidSdkShort);
return false;
}
if (!_canRunSdkManager()) {
throwToolExit(
'Android sdkmanager not found. Update to the latest Android SDK and ensure that '
'the cmdline-tools are installed to resolve this.'
);
}
try {
final Process process = await _processManager.start(
<String>[_androidSdk!.sdkManagerPath!, '--licenses'],
environment: _androidSdk!.sdkManagerEnv,
);
// The real stdin will never finish streaming. Pipe until the child process
// finishes.
unawaited(process.stdin.addStream(_stdio.stdin)
// If the process exits unexpectedly with an error, that will be
// handled by the caller.
.catchError((dynamic err, StackTrace stack) {
_logger.printTrace('Echoing stdin to the licenses subprocess failed:');
_logger.printTrace('$err\n$stack');
}
));
// Wait for stdout and stderr to be fully processed, because process.exitCode
// may complete first.
try {
await Future.wait<void>(<Future<void>>[
_stdio.addStdoutStream(process.stdout),
_stdio.addStderrStream(process.stderr),
]);
} on Exception catch (err, stack) {
_logger.printTrace('Echoing stdout or stderr from the license subprocess failed:');
_logger.printTrace('$err\n$stack');
}
final int exitCode = await process.exitCode;
return exitCode == 0;
} on ProcessException catch (e) {
throwToolExit(_userMessages.androidCannotRunSdkManager(
_androidSdk?.sdkManagerPath ?? '',
e.toString(),
_platform,
));
}
}
bool _canRunSdkManager() {
final String? sdkManagerPath = _androidSdk?.sdkManagerPath;
if (sdkManagerPath == null) {
return false;
}
return _processManager.canRun(sdkManagerPath);
}
}