diff --git a/packages/flutter_tools/lib/src/android/android_workflow.dart b/packages/flutter_tools/lib/src/android/android_workflow.dart index 7b6b3e6642..1bd62c5d11 100644 --- a/packages/flutter_tools/lib/src/android/android_workflow.dart +++ b/packages/flutter_tools/lib/src/android/android_workflow.dart @@ -19,6 +19,7 @@ import '../globals.dart'; import 'android_sdk.dart'; const int kAndroidSdkMinVersion = 28; +final Version kAndroidJavaMinVersion = Version(1, 8, 0); final Version kAndroidSdkBuildToolsMinVersion = Version(28, 0, 3); AndroidWorkflow get androidWorkflow => context.get(); @@ -57,8 +58,19 @@ class AndroidValidator extends DoctorValidator { 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 from that response. + static String _extractJavaVersion(String text) { + final Match match = _javaVersionPattern.firstMatch(text ?? ''); + return text?.substring(match.start, match.end); + } + /// Returns false if we cannot determine the Java version or if the version - /// is not compatible. + /// is older that the minimum allowed version of 1.8. Future _checkJavaVersion(String javaBinary, List messages) async { _task = 'Checking Java status'; try { @@ -66,24 +78,28 @@ class AndroidValidator extends DoctorValidator { messages.add(ValidationMessage.error(userMessages.androidCantRunJavaBinary(javaBinary))); return false; } - String javaVersion; + String javaVersionText; try { printTrace('java -version'); final ProcessResult result = await processManager.run([javaBinary, '-version']); if (result.exitCode == 0) { final List versionLines = result.stderr.split('\n'); - javaVersion = versionLines.length >= 2 ? versionLines[1] : versionLines[0]; + javaVersionText = versionLines.length >= 2 ? versionLines[1] : versionLines[0]; } } catch (error) { printTrace(error.toString()); } - if (javaVersion == null) { + if (javaVersionText == null || javaVersionText.isEmpty) { // Could not determine the java version. messages.add(ValidationMessage.error(userMessages.androidUnknownJavaVersion)); return false; } - messages.add(ValidationMessage(userMessages.androidJavaVersion(javaVersion))); - // TODO(johnmccutchan): Validate version. + 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))); return true; } finally { _task = null; diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart index 2a078bc4b5..a071f01d00 100644 --- a/packages/flutter_tools/lib/src/base/user_messages.dart +++ b/packages/flutter_tools/lib/src/base/user_messages.dart @@ -49,6 +49,7 @@ class UserMessages { String androidCantRunJavaBinary(String javaBinary) => 'Cannot execute $javaBinary to determine the version'; String get androidUnknownJavaVersion => 'Could not determine java version'; String androidJavaVersion(String javaVersion) => 'Java version $javaVersion'; + String androidJavaMinimumVersion(String javaVersion) => 'Java version $javaVersion is older than the minimum recommended version of 1.8'; String androidSdkLicenseOnly(String envKey) => 'Android SDK contains licenses only.\n' 'Your first build of an Android application will take longer than usual, ' diff --git a/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart b/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart index b4a08cdd90..5807ef918e 100644 --- a/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart +++ b/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart @@ -48,13 +48,13 @@ void main() { final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(); final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted; expect(licenseStatus, LicensesAccepted.unknown); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('licensesAccepted returns LicensesAccepted.unknown if cannot run sdkmanager', () async { processManager.runSucceeds = false; @@ -62,13 +62,13 @@ void main() { final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(); final LicensesAccepted licenseStatus = await licenseValidator.licensesAccepted; expect(licenseStatus, LicensesAccepted.unknown); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('licensesAccepted handles garbage/no output', () async { when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); @@ -77,13 +77,13 @@ void main() { expect(result, equals(LicensesAccepted.unknown)); expect(processManager.commands.first, equals('/foo/bar/sdkmanager')); expect(processManager.commands.last, equals('--licenses')); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('licensesAccepted works for all licenses accepted', () async { when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); @@ -95,13 +95,13 @@ void main() { final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(); final LicensesAccepted result = await licenseValidator.licensesAccepted; expect(result, equals(LicensesAccepted.all)); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('licensesAccepted works for some licenses accepted', () async { when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); @@ -114,13 +114,13 @@ void main() { final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(); final LicensesAccepted result = await licenseValidator.licensesAccepted; expect(result, equals(LicensesAccepted.some)); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('licensesAccepted works for no licenses accepted', () async { when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); @@ -133,78 +133,78 @@ void main() { final AndroidLicenseValidator licenseValidator = AndroidLicenseValidator(); final LicensesAccepted result = await licenseValidator.licensesAccepted; expect(result, equals(LicensesAccepted.none)); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('runLicenseManager succeeds for version >= 26', () async { when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); when(sdk.sdkManagerVersion).thenReturn('26.0.0'); expect(await AndroidLicenseValidator.runLicenseManager(), isTrue); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('runLicenseManager errors for version < 26', () async { when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); when(sdk.sdkManagerVersion).thenReturn('25.0.0'); expect(AndroidLicenseValidator.runLicenseManager(), throwsToolExit(message: 'To update, run')); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('runLicenseManager errors correctly for null version', () async { when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); when(sdk.sdkManagerVersion).thenReturn(null); expect(AndroidLicenseValidator.runLicenseManager(), throwsToolExit(message: 'To update, run')); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('runLicenseManager errors when sdkmanager is not found', () async { when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); processManager.canRunSucceeds = false; expect(AndroidLicenseValidator.runLicenseManager(), throwsToolExit()); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('runLicenseManager errors when sdkmanager fails to run', () async { when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); processManager.runSucceeds = false; expect(AndroidLicenseValidator.runLicenseManager(), throwsToolExit()); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('detects license-only SDK installation', () async { when(sdk.licensesAvailable).thenReturn(true); @@ -215,13 +215,13 @@ void main() { validationResult.messages.last.message, userMessages.androidSdkLicenseOnly(kAndroidHome), ); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); testUsingContext('detects minium required SDK and buildtools', () async { final AndroidSdkVersion mockSdkVersion = MockAndroidSdkVersion(); @@ -269,12 +269,44 @@ void main() { validationResult.messages.any((ValidationMessage message) => message.message == errorMessage), isFalse, ); - }, overrides: { + }, overrides: Map.unmodifiable({ AndroidSdk: () => sdk, FileSystem: () => fs, ProcessManager: () => processManager, Platform: () => FakePlatform()..environment = {'HOME': '/home/me'}, Stdio: () => stdio, - }); + })); + + testUsingContext('detects minimum required java version', () async { + final AndroidSdkVersion mockSdkVersion = MockAndroidSdkVersion(); + + // Mock a pass through scenario to reach _checkJavaVersion() + when(sdk.licensesAvailable).thenReturn(true); + when(sdk.platformToolsAvailable).thenReturn(true); + when(mockSdkVersion.sdkLevel).thenReturn(28); + when(mockSdkVersion.buildToolsVersion).thenReturn(Version(28, 0, 3)); + when(sdk.sdkManagerPath).thenReturn('/foo/bar/sdkmanager'); + when(sdk.latestVersion).thenReturn(mockSdkVersion); + when(sdk.validateSdkWellFormed()).thenReturn([]); + + //Test with older version of JDK + const String javaVersionText = 'openjdk version "1.7.0_212"'; + when(processManager.run(argThat(contains('-version')))).thenAnswer((_) => + Future.value(ProcessResult(0, 0, null, javaVersionText))); + final String errorMessage = userMessages.androidJavaMinimumVersion(javaVersionText); + + final ValidationResult validationResult = await AndroidValidator().validate(); + expect(validationResult.type, ValidationType.partial); + expect( + validationResult.messages.last.message, + errorMessage, + ); + }, overrides: Map.unmodifiable({ + AndroidSdk: () => sdk, + FileSystem: () => fs, + Platform: () => FakePlatform()..environment = {'HOME': '/home/me', 'JAVA_HOME': 'home/java'}, + ProcessManager: () => processManager, + Stdio: () => stdio, + })); }