1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:process/process.dart' ; |
6 | |
7 | import '../base/config.dart'; |
8 | import '../base/file_system.dart'; |
9 | import '../base/logger.dart'; |
10 | import '../base/os.dart'; |
11 | import '../base/platform.dart'; |
12 | import '../base/process.dart'; |
13 | import '../base/version.dart'; |
14 | import 'android_studio.dart'; |
15 | |
16 | const String _javaExecutable = 'java' ; |
17 | |
18 | enum JavaSource { |
19 | /// JDK bundled with latest Android Studio installation. |
20 | androidStudio, |
21 | |
22 | /// JDK specified by the system's JAVA_HOME environment variable. |
23 | javaHome, |
24 | |
25 | /// JDK available through the system's PATH environment variable. |
26 | path, |
27 | |
28 | /// JDK specified in Flutter's configuration. |
29 | flutterConfig, |
30 | } |
31 | |
32 | typedef _JavaHomePathWithSource = ({String path, JavaSource source}); |
33 | |
34 | /// Represents an installation of Java. |
35 | class Java { |
36 | Java({ |
37 | required this.javaHome, |
38 | required this.binaryPath, |
39 | required this.javaSource, |
40 | required Logger logger, |
41 | required FileSystem fileSystem, |
42 | required OperatingSystemUtils os, |
43 | required Platform platform, |
44 | required ProcessManager processManager, |
45 | }) : _logger = logger, |
46 | _fileSystem = fileSystem, |
47 | _os = os, |
48 | _platform = platform, |
49 | _processManager = processManager, |
50 | _processUtils = ProcessUtils(processManager: processManager, logger: logger); |
51 | |
52 | /// Within the Java ecosystem, this environment variable is typically set |
53 | /// the install location of a Java Runtime Environment (JRE) or Java |
54 | /// Development Kit (JDK). |
55 | /// |
56 | /// Tools that depend on Java and need to find it will often check this |
57 | /// variable. If you are looking to set `JAVA_HOME` when stating a process, |
58 | /// consider using the [environment] instance property instead. |
59 | static String javaHomeEnvironmentVariable = 'JAVA_HOME' ; |
60 | |
61 | /// Finds the Java runtime environment that should be used for all java-dependent |
62 | /// operations across the tool. |
63 | /// |
64 | /// This searches for Java in the following places, in order: |
65 | /// |
66 | /// 1. the runtime environment bundled with Android Studio; |
67 | /// 2. the runtime environment found in the JAVA_HOME env variable, if set; or |
68 | /// 3. the java binary found on PATH. |
69 | /// |
70 | /// Returns null if no java binary could be found. |
71 | static Java? find({ |
72 | required Config config, |
73 | required AndroidStudio? androidStudio, |
74 | required Logger logger, |
75 | required FileSystem fileSystem, |
76 | required Platform platform, |
77 | required ProcessManager processManager, |
78 | }) { |
79 | final OperatingSystemUtils os = OperatingSystemUtils( |
80 | fileSystem: fileSystem, |
81 | logger: logger, |
82 | platform: platform, |
83 | processManager: processManager, |
84 | ); |
85 | final _JavaHomePathWithSource? home = _findJavaHome( |
86 | config: config, |
87 | logger: logger, |
88 | androidStudio: androidStudio, |
89 | platform: platform, |
90 | ); |
91 | final String? binary = _findJavaBinary( |
92 | logger: logger, |
93 | javaHome: home?.path, |
94 | fileSystem: fileSystem, |
95 | operatingSystemUtils: os, |
96 | platform: platform, |
97 | ); |
98 | |
99 | if (binary == null) { |
100 | return null; |
101 | } |
102 | |
103 | // If javaHome == null and binary is not null, it means that |
104 | // binary obtained from PATH as fallback. |
105 | final JavaSource javaSource = home?.source ?? JavaSource.path; |
106 | |
107 | return Java( |
108 | javaHome: home?.path, |
109 | binaryPath: binary, |
110 | javaSource: javaSource, |
111 | logger: logger, |
112 | fileSystem: fileSystem, |
113 | os: os, |
114 | platform: platform, |
115 | processManager: processManager, |
116 | ); |
117 | } |
118 | |
119 | /// The path of the runtime environments' home directory. |
120 | /// |
121 | /// This should only be used for logging and validation purposes. |
122 | /// If you need to set JAVA_HOME when starting a process, consider |
123 | /// using [environment] instead. |
124 | /// If you need to inspect the files of the runtime, considering adding |
125 | /// a new method to this class instead. |
126 | final String? javaHome; |
127 | |
128 | /// The path of the runtime environments' java binary. |
129 | /// |
130 | /// This should be only used for logging and validation purposes. |
131 | /// If you need to invoke the binary directly, consider adding a new method |
132 | /// to this class instead. |
133 | final String binaryPath; |
134 | |
135 | /// Indicates the source from where the Java runtime was located. |
136 | /// |
137 | /// This information is useful for debugging and logging purposes to track |
138 | /// which source was used to locate the Java runtime environment. |
139 | final JavaSource javaSource; |
140 | |
141 | final Logger _logger; |
142 | final FileSystem _fileSystem; |
143 | final OperatingSystemUtils _os; |
144 | final Platform _platform; |
145 | final ProcessManager _processManager; |
146 | final ProcessUtils _processUtils; |
147 | |
148 | /// Returns an environment variable map with |
149 | /// 1. JAVA_HOME set if this object has a known home directory, and |
150 | /// 2. The java binary folder appended onto PATH, if the binary location is known. |
151 | /// |
152 | /// This map should be used as the environment when invoking any Java-dependent |
153 | /// processes, such as Gradle or Android SDK tools (avdmanager, sdkmanager, etc.) |
154 | Map<String, String> get environment => <String, String>{ |
155 | if (javaHome != null) javaHomeEnvironmentVariable: javaHome!, |
156 | 'PATH' : |
157 | _fileSystem.path.dirname(binaryPath) + |
158 | _os.pathVarSeparator + |
159 | _platform.environment['PATH' ]!, |
160 | }; |
161 | |
162 | /// Returns the version of java in the format \d(.\d)+(.\d)+ |
163 | /// Returns null if version could not be determined. |
164 | late final Version? version = |
165 | (() { |
166 | if (!canRun()) { |
167 | return null; |
168 | } |
169 | |
170 | final RunResult result = _processUtils.runSync(<String>[ |
171 | binaryPath, |
172 | '--version' , |
173 | ], environment: environment); |
174 | if (result.exitCode != 0) { |
175 | _logger.printTrace( |
176 | 'java --version failed: exitCode: ${result.exitCode}' |
177 | ' stdout: ${result.stdout} stderr: ${result.stderr}' , |
178 | ); |
179 | return null; |
180 | } |
181 | final String rawVersionOutput = result.stdout; |
182 | final List<String> versionLines = rawVersionOutput.split('\n' ); |
183 | // Should look something like 'openjdk 19.0.2 2023-01-17'. |
184 | final String longVersionText = versionLines.length >= 2 ? versionLines[1] : versionLines[0]; |
185 | |
186 | // The contents that matter come in the format '11.0.18', '1.8.0_202 or 21'. |
187 | final RegExp jdkVersionRegex = RegExp(r'(?<version>\d+(\.\d+(\.\d+(?:_\d+)?)?)?)' ); |
188 | final Iterable<RegExpMatch> matches = jdkVersionRegex.allMatches(rawVersionOutput); |
189 | if (matches.isEmpty) { |
190 | // Fallback to second string format like "java 21.0.1 2023-09-19 LTS" |
191 | final RegExp secondJdkVersionRegex = RegExp( |
192 | r'java\s+(?<version>\d+(\.\d+)?(\.\d+)?)\s+\d\d\d\d-\d\d-\d\d' , |
193 | ); |
194 | final RegExpMatch? match = secondJdkVersionRegex.firstMatch(versionLines[0]); |
195 | if (match != null) { |
196 | return Version.parse(match.namedGroup('version' )); |
197 | } |
198 | _logger.printWarning(_formatJavaVersionWarning(rawVersionOutput)); |
199 | return null; |
200 | } |
201 | final String? version = matches.first.namedGroup('version' ); |
202 | if (version == null || version.split('_' ).isEmpty) { |
203 | _logger.printWarning(_formatJavaVersionWarning(rawVersionOutput)); |
204 | return null; |
205 | } |
206 | |
207 | // Trim away _d+ from versions 1.8 and below. |
208 | final String versionWithoutBuildInfo = version.split('_' ).first; |
209 | |
210 | final Version? parsedVersion = Version.parse(versionWithoutBuildInfo); |
211 | if (parsedVersion == null) { |
212 | return null; |
213 | } |
214 | return Version.withText( |
215 | parsedVersion.major, |
216 | parsedVersion.minor, |
217 | parsedVersion.patch, |
218 | longVersionText, |
219 | ); |
220 | })(); |
221 | |
222 | bool canRun() { |
223 | return _processManager.canRun(binaryPath); |
224 | } |
225 | } |
226 | |
227 | _JavaHomePathWithSource? _findJavaHome({ |
228 | required Config config, |
229 | required Logger logger, |
230 | required AndroidStudio? androidStudio, |
231 | required Platform platform, |
232 | }) { |
233 | final Object? configured = config.getValue('jdk-dir' ); |
234 | if (configured != null) { |
235 | return (path: configured as String, source: JavaSource.flutterConfig); |
236 | } |
237 | |
238 | final String? androidStudioJavaPath = androidStudio?.javaPath; |
239 | if (androidStudioJavaPath != null) { |
240 | return (path: androidStudioJavaPath, source: JavaSource.androidStudio); |
241 | } |
242 | |
243 | final String? javaHomeEnv = platform.environment[Java.javaHomeEnvironmentVariable]; |
244 | if (javaHomeEnv != null) { |
245 | return (path: javaHomeEnv, source: JavaSource.javaHome); |
246 | } |
247 | return null; |
248 | } |
249 | |
250 | String? _findJavaBinary({ |
251 | required Logger logger, |
252 | required String? javaHome, |
253 | required FileSystem fileSystem, |
254 | required OperatingSystemUtils operatingSystemUtils, |
255 | required Platform platform, |
256 | }) { |
257 | if (javaHome != null) { |
258 | return fileSystem.path.join(javaHome, 'bin' , 'java' ); |
259 | } |
260 | |
261 | // Fallback to PATH based lookup. |
262 | return operatingSystemUtils.which(_javaExecutable)?.path; |
263 | } |
264 | |
265 | // Returns a user visible String that says the tool failed to parse |
266 | // the version of java along with the output. |
267 | String _formatJavaVersionWarning(String javaVersionRaw) { |
268 | return 'Could not parse java version from: \n' |
269 | ' $javaVersionRaw \n' |
270 | 'If there is a version please look for an existing bug ' |
271 | 'https://github.com/flutter/flutter/issues/ ' |
272 | 'and if one does not exist file a new issue.' ; |
273 | } |
274 | |