Skip to content

Commit 4b46b80

Browse files
doctor: make JDK validation message more descriptive (#157280)
This PR attempts to improve clarity of androids section of `flutter doctor -v` output by providing explicit information about which JDK is being used and how to configure a different one if needed. ### Before ```console • Java binary at: /Users/user/Applications/Android Studio Ladybug Feature Drop 2024.2.2 Canary 2.app/Contents/jbr/Contents/Home/bin/java ``` ### After 1. When JDK is from Android Studio: ```console • Java binary at: /Users/users/Applications/Android Studio Ladybug Feature Drop 2024.2.2 Canary 2.app/Contents/jbr/Contents/Home/bin/java This is the JDK bundled with latest Android Studio installation To manually set a custom JDK path, use: `flutter config --jdk-dir="path/to/jdk"` ``` 2. When JDK is from JAVA_HOME env variable: ```console • Java binary at: /Users/user/Applications/Android Studio Ladybug Feature Drop 2024.2.2 Canary 2.app/Contents/jbr/Contents/Home/bin/java This JDK is specified by JAVA_HOME environment variable To manually set a custom JDK path, use: `flutter config --jdk-dir="path/to/jdk"` ``` 3. When path to JDK is set in flutter config: ```console • Java binary at: /Users/user/Applications/Android Studio Ladybug Feature Drop 2024.2.2 Canary 2.app/Contents/jbr/Contents/Home/bin/java This JDK was found in system PATH To change current JDK, run: `flutter config --jdk-dir="path/to/jdk"` ``` 4. When java binary is found in PATH (as fallback) ```console • Java binary at: /Users/user/Applications/Android Studio Ladybug Feature Drop 2024.2.2 Canary 2.app/Contents/jbr/Contents/Home/bin/java This JDK is specified in Flutter configuration To manually set a custom JDK path, use: `flutter config --jdk-dir="path/to/jdk"` ``` ### Motivation I think it's described in flutter/flutter#153156 (comment). TLDR; many developers struggle with Java-related issues and more verbose doctor's output will (presumably) improve DX in that part. fixes #153156 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Andrew Kolos <[email protected]>
1 parent 5efd759 commit 4b46b80

File tree

6 files changed

+218
-9
lines changed

6 files changed

+218
-9
lines changed

packages/flutter_tools/lib/src/android/android_workflow.dart

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,12 @@ class AndroidValidator extends DoctorValidator {
105105
messages.add(ValidationMessage.error(_userMessages.androidMissingJdk));
106106
return false;
107107
}
108-
messages.add(ValidationMessage(_userMessages.androidJdkLocation(_java!.binaryPath)));
108+
messages.add(ValidationMessage(
109+
_androidJdkLocationMessage(
110+
_java!.binaryPath,
111+
_java.javaSource,
112+
),
113+
));
109114
if (!_java.canRun()) {
110115
messages.add(ValidationMessage.error(_userMessages.androidCantRunJavaBinary(_java.binaryPath)));
111116
return false;
@@ -454,3 +459,26 @@ class AndroidLicenseValidator extends DoctorValidator {
454459
);
455460
}
456461
}
462+
463+
String _androidJdkLocationMessage(String location, JavaSource source) {
464+
final String setWithConfigBreadcrumb = switch (source) {
465+
JavaSource.androidStudio || JavaSource.path || JavaSource.javaHome =>
466+
'To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.',
467+
JavaSource.flutterConfig =>
468+
'To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.'
469+
};
470+
final String sourceMessagePart = switch (source) {
471+
JavaSource.androidStudio =>
472+
'This is the JDK bundled with the latest Android Studio installation on this machine.',
473+
JavaSource.javaHome =>
474+
'This JDK is specified by the JAVA_HOME environment variable.',
475+
JavaSource.path =>
476+
'This JDK was found in the system PATH.',
477+
JavaSource.flutterConfig =>
478+
'This JDK is specified in your Flutter configuration.',
479+
};
480+
481+
return 'Java binary at: $location\n'
482+
'$sourceMessagePart\n'
483+
'$setWithConfigBreadcrumb';
484+
}

packages/flutter_tools/lib/src/android/java.dart

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,25 @@ import 'android_studio.dart';
1515

1616
const String _javaExecutable = 'java';
1717

18+
enum JavaSource {
19+
/// JDK bundled with latest Android Studio installation.
20+
androidStudio,
21+
/// JDK specified by the system's JAVA_HOME environment variable.
22+
javaHome,
23+
/// JDK available through the system's PATH environment variable.
24+
path,
25+
/// JDK specified in Flutter's configuration.
26+
flutterConfig,
27+
}
28+
29+
typedef _JavaHomePathWithSource = ({String path, JavaSource source});
30+
1831
/// Represents an installation of Java.
1932
class Java {
2033
Java({
2134
required this.javaHome,
2235
required this.binaryPath,
36+
required this.javaSource,
2337
required Logger logger,
2438
required FileSystem fileSystem,
2539
required OperatingSystemUtils os,
@@ -65,15 +79,15 @@ class Java {
6579
platform: platform,
6680
processManager: processManager
6781
);
68-
final String? home = _findJavaHome(
82+
final _JavaHomePathWithSource? home = _findJavaHome(
6983
config: config,
7084
logger: logger,
7185
androidStudio: androidStudio,
7286
platform: platform
7387
);
7488
final String? binary = _findJavaBinary(
7589
logger: logger,
76-
javaHome: home,
90+
javaHome: home?.path,
7791
fileSystem: fileSystem,
7892
operatingSystemUtils: os,
7993
platform: platform
@@ -83,9 +97,14 @@ class Java {
8397
return null;
8498
}
8599

100+
// If javaHome == null and binary is not null, it means that
101+
// binary obtained from PATH as fallback.
102+
final JavaSource javaSource = home?.source ?? JavaSource.path;
103+
86104
return Java(
87-
javaHome: home,
105+
javaHome: home?.path,
88106
binaryPath: binary,
107+
javaSource: javaSource,
89108
logger: logger,
90109
fileSystem: fileSystem,
91110
os: os,
@@ -110,6 +129,12 @@ class Java {
110129
/// to this class instead.
111130
final String binaryPath;
112131

132+
/// Indicates the source from where the Java runtime was located.
133+
///
134+
/// This information is useful for debugging and logging purposes to track
135+
/// which source was used to locate the Java runtime environment.
136+
final JavaSource javaSource;
137+
113138
final Logger _logger;
114139
final FileSystem _fileSystem;
115140
final OperatingSystemUtils _os;
@@ -192,25 +217,25 @@ class Java {
192217
}
193218
}
194219

195-
String? _findJavaHome({
220+
_JavaHomePathWithSource? _findJavaHome({
196221
required Config config,
197222
required Logger logger,
198223
required AndroidStudio? androidStudio,
199224
required Platform platform,
200225
}) {
201226
final Object? configured = config.getValue('jdk-dir');
202227
if (configured != null) {
203-
return configured as String;
228+
return (path: configured as String, source: JavaSource.flutterConfig);
204229
}
205230

206231
final String? androidStudioJavaPath = androidStudio?.javaPath;
207232
if (androidStudioJavaPath != null) {
208-
return androidStudioJavaPath;
233+
return (path: androidStudioJavaPath, source: JavaSource.androidStudio);
209234
}
210235

211236
final String? javaHomeEnv = platform.environment[Java.javaHomeEnvironmentVariable];
212237
if (javaHomeEnv != null) {
213-
return javaHomeEnv;
238+
return (path: javaHomeEnv, source: JavaSource.javaHome);
214239
}
215240
return null;
216241
}

packages/flutter_tools/lib/src/base/user_messages.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ class UserMessages {
115115
'No Java Development Kit (JDK) found; You must have the environment '
116116
'variable JAVA_HOME set and the java binary in your PATH. '
117117
'You can download the JDK from https://www.oracle.com/technetwork/java/javase/downloads/.';
118-
String androidJdkLocation(String location) => 'Java binary at: $location';
119118
String get androidLicensesAll => 'All Android licenses accepted.';
120119
String get androidLicensesSome => 'Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses';
121120
String get androidLicensesNone => 'Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses';

packages/flutter_tools/test/general.shard/android/android_workflow_test.dart

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,146 @@ Android sdkmanager tool was found, but failed to run
637637
expect(processManager, hasNoRemainingExpectations);
638638
expect(stdio.stderr.getAndClear(), contains('UnsupportedClassVersionError'));
639639
});
640+
641+
testWithoutContext('Mentions that JDK is provided by latest Android Studio Installation', () async {
642+
// Mock a pass through scenario to reach _checkJavaVersion()
643+
sdk
644+
..licensesAvailable = true
645+
..platformToolsAvailable = true
646+
..cmdlineToolsAvailable = true
647+
..directory = fileSystem.directory('/foo/bar')
648+
..sdkManagerPath = '/foo/bar/sdkmanager';
649+
650+
final ValidationResult validationResult = await AndroidValidator(
651+
java: FakeJava(),
652+
androidSdk: sdk,
653+
logger: logger,
654+
platform: FakePlatform(),
655+
userMessages: UserMessages()
656+
).validate();
657+
658+
expect(
659+
validationResult.messages.any(
660+
(ValidationMessage message) => message.message.contains(
661+
'This is the JDK bundled with the latest Android Studio installation on this machine.'
662+
)
663+
),
664+
true,
665+
);
666+
expect(
667+
validationResult.messages.any(
668+
(ValidationMessage message) => message.message.contains(
669+
'To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.'
670+
)
671+
),
672+
true,
673+
);
674+
});
675+
676+
testWithoutContext("Mentions that JDK is provided by user's JAVA_HOME environment variable", () async {
677+
// Mock a pass through scenario to reach _checkJavaVersion()
678+
sdk
679+
..licensesAvailable = true
680+
..platformToolsAvailable = true
681+
..cmdlineToolsAvailable = true
682+
..directory = fileSystem.directory('/foo/bar')
683+
..sdkManagerPath = '/foo/bar/sdkmanager';
684+
685+
final ValidationResult validationResult = await AndroidValidator(
686+
java: FakeJava(javaSource: JavaSource.javaHome),
687+
androidSdk: sdk,
688+
logger: logger,
689+
platform: FakePlatform(),
690+
userMessages: UserMessages()
691+
).validate();
692+
693+
expect(
694+
validationResult.messages.any(
695+
(ValidationMessage message) => message.message.contains(
696+
'This JDK is specified by the JAVA_HOME environment variable.'
697+
)
698+
),
699+
true,
700+
);
701+
expect(
702+
validationResult.messages.any(
703+
(ValidationMessage message) => message.message.contains(
704+
'To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`'
705+
)
706+
),
707+
true,
708+
);
709+
});
710+
711+
testWithoutContext('Mentions that path to Java binary is obtained from PATH', () async {
712+
// Mock a pass through scenario to reach _checkJavaVersion()
713+
sdk
714+
..licensesAvailable = true
715+
..platformToolsAvailable = true
716+
..cmdlineToolsAvailable = true
717+
..directory = fileSystem.directory('/foo/bar')
718+
..sdkManagerPath = '/foo/bar/sdkmanager';
719+
720+
final ValidationResult validationResult = await AndroidValidator(
721+
java: FakeJava(javaSource: JavaSource.path),
722+
androidSdk: sdk,
723+
logger: logger,
724+
platform: FakePlatform(),
725+
userMessages: UserMessages()
726+
).validate();
727+
728+
expect(
729+
validationResult.messages.any(
730+
(ValidationMessage message) => message.message.contains(
731+
'This JDK was found in the system PATH.'
732+
)
733+
),
734+
true,
735+
);
736+
expect(
737+
validationResult.messages.any(
738+
(ValidationMessage message) => message.message.contains(
739+
'To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.'
740+
)
741+
),
742+
true,
743+
);
744+
});
745+
746+
testWithoutContext('Mentions that JDK is provided by Flutter config', () async {
747+
// Mock a pass through scenario to reach _checkJavaVersion()
748+
sdk
749+
..licensesAvailable = true
750+
..platformToolsAvailable = true
751+
..cmdlineToolsAvailable = true
752+
..directory = fileSystem.directory('/foo/bar')
753+
..sdkManagerPath = '/foo/bar/sdkmanager';
754+
755+
final ValidationResult validationResult = await AndroidValidator(
756+
java: FakeJava(javaSource: JavaSource.flutterConfig),
757+
androidSdk: sdk,
758+
logger: logger,
759+
platform: FakePlatform(),
760+
userMessages: UserMessages()
761+
).validate();
762+
763+
expect(
764+
validationResult.messages.any(
765+
(ValidationMessage message) => message.message.contains(
766+
'This JDK is specified in your Flutter configuration.'
767+
)
768+
),
769+
true,
770+
);
771+
expect(
772+
validationResult.messages.any(
773+
(ValidationMessage message) => message.message.contains(
774+
'To change the current JDK, run: `flutter config --jdk-dir="path/to/jdk"`.'
775+
)
776+
),
777+
true,
778+
);
779+
});
640780
}
641781

642782
class FakeAndroidSdk extends Fake implements AndroidSdk {

packages/flutter_tools/test/general.shard/android/java_test.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ void main() {
4444
final AndroidStudio androidStudio = _FakeAndroidStudioWithJdk();
4545
final String androidStudioBundledJdkHome = androidStudio.javaPath!;
4646
final String expectedJavaBinaryPath = fs.path.join(androidStudioBundledJdkHome, 'bin', 'java');
47+
const JavaSource expectedJavaHomeSource = JavaSource.androidStudio;
4748

4849
processManager.addCommand(FakeCommand(
4950
command: <String>[
@@ -70,12 +71,14 @@ OpenJDK 64-Bit Server VM Zulu19.32+15-CA (build 19.0.2+7, mixed mode, sharing)
7071

7172
expect(java.version!.toString(), 'OpenJDK Runtime Environment Zulu19.32+15-CA (build 19.0.2+7)');
7273
expect(java.version, equals(Version(19, 0, 2)));
74+
expect(java.javaSource, expectedJavaHomeSource);
7375
});
7476

7577
testWithoutContext('finds JAVA_HOME if it is set and the JDK bundled with Android Studio could not be found', () {
7678
final AndroidStudio androidStudio = _FakeAndroidStudioWithoutJdk();
7779
const String javaHome = '/java/home';
7880
final String expectedJavaBinaryPath = fs.path.join(javaHome, 'bin', 'java');
81+
const JavaSource expectedJavaHomeSource = JavaSource.javaHome;
7982

8083
final Java java = Java.find(
8184
config: config,
@@ -90,11 +93,14 @@ OpenJDK 64-Bit Server VM Zulu19.32+15-CA (build 19.0.2+7, mixed mode, sharing)
9093

9194
expect(java.javaHome, javaHome);
9295
expect(java.binaryPath, expectedJavaBinaryPath);
96+
expect(java.javaSource, expectedJavaHomeSource);
9397
});
9498

9599
testWithoutContext('returns the java binary found on PATH if no other can be found', () {
96100
final AndroidStudio androidStudio = _FakeAndroidStudioWithoutJdk();
97101
final OperatingSystemUtils os = _FakeOperatingSystemUtilsWithJava(fileSystem);
102+
const JavaSource expectedJavaHomeSource = JavaSource.path;
103+
98104

99105
processManager.addCommand(
100106
const FakeCommand(
@@ -114,6 +120,7 @@ OpenJDK 64-Bit Server VM Zulu19.32+15-CA (build 19.0.2+7, mixed mode, sharing)
114120

115121
expect(java.javaHome, isNull);
116122
expect(java.binaryPath, os.which('java')!.path);
123+
expect(java.javaSource, expectedJavaHomeSource);
117124
});
118125

119126
testWithoutContext('returns null if no java could be found', () {
@@ -138,6 +145,7 @@ OpenJDK 64-Bit Server VM Zulu19.32+15-CA (build 19.0.2+7, mixed mode, sharing)
138145
testWithoutContext('finds and prefers JDK found at config item "jdk-dir" if it is set', () {
139146
const String configuredJdkPath = '/jdk';
140147
config.setValue('jdk-dir', configuredJdkPath);
148+
JavaSource expectedJavaHomeSource = JavaSource.flutterConfig;
141149

142150
processManager.addCommand(
143151
const FakeCommand(
@@ -164,9 +172,12 @@ OpenJDK 64-Bit Server VM Zulu19.32+15-CA (build 19.0.2+7, mixed mode, sharing)
164172
expect(java, isNotNull);
165173
expect(java!.javaHome, configuredJdkPath);
166174
expect(java.binaryPath, fs.path.join(configuredJdkPath, 'bin', 'java'));
175+
expect(java.javaSource, expectedJavaHomeSource);
167176

168177
config.removeValue('jdk-dir');
169178

179+
expectedJavaHomeSource = JavaSource.androidStudio;
180+
170181
java = Java.find(
171182
config: config,
172183
androidStudio: androidStudio,
@@ -180,6 +191,7 @@ OpenJDK 64-Bit Server VM Zulu19.32+15-CA (build 19.0.2+7, mixed mode, sharing)
180191
assert(androidStudio.javaPath != configuredJdkPath);
181192
expect(java!.javaHome, androidStudio.javaPath);
182193
expect(java.binaryPath, fs.path.join(androidStudio.javaPath!, 'bin', 'java'));
194+
expect(java.javaSource, expectedJavaHomeSource);
183195
});
184196
});
185197

@@ -196,6 +208,7 @@ OpenJDK 64-Bit Server VM Zulu19.32+15-CA (build 19.0.2+7, mixed mode, sharing)
196208
processManager: processManager,
197209
binaryPath: 'javaHome/bin/java',
198210
javaHome: 'javaHome',
211+
javaSource: JavaSource.javaHome,
199212
);
200213
});
201214

packages/flutter_tools/test/src/fakes.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,7 @@ class FakeAndroidStudio extends Fake implements AndroidStudio {
670670
class FakeJava extends Fake implements Java {
671671
FakeJava({
672672
this.javaHome = '/android-studio/jbr',
673+
this.javaSource = JavaSource.androidStudio,
673674
String binary = '/android-studio/jbr/bin/java',
674675
Version? version,
675676
bool canRun = true,
@@ -687,6 +688,9 @@ class FakeJava extends Fake implements Java {
687688
@override
688689
String binaryPath;
689690

691+
@override
692+
JavaSource javaSource;
693+
690694
final Map<String, String> _environment;
691695
final bool _canRun;
692696

0 commit comments

Comments
 (0)