blob: 8830ef69dda604a3bfe856d1771169514d713bc6 [file] [log] [blame]
Andrew Kolos80a4f9b2023-05-20 06:29:21 +00001// 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
5import 'package:process/process.dart';
6
Andrew Kolos96afa502023-06-07 04:52:17 +00007import '../base/config.dart';
Andrew Kolos80a4f9b2023-05-20 06:29:21 +00008import '../base/file_system.dart';
9import '../base/logger.dart';
10import '../base/os.dart';
11import '../base/platform.dart';
12import '../base/process.dart';
Andrew Kolos06e2b1812023-06-01 04:19:19 +000013import '../base/version.dart';
Andrew Kolos80a4f9b2023-05-20 06:29:21 +000014import 'android_studio.dart';
15
Andrew Kolos06e2b1812023-06-01 04:19:19 +000016const String _javaExecutable = 'java';
Andrew Kolos80a4f9b2023-05-20 06:29:21 +000017
18/// Represents an installation of Java.
19class Java {
20 Java({
21 required this.javaHome,
22 required this.binaryPath,
23 required Logger logger,
24 required FileSystem fileSystem,
25 required OperatingSystemUtils os,
26 required Platform platform,
27 required ProcessManager processManager,
28 }): _logger = logger,
29 _fileSystem = fileSystem,
30 _os = os,
31 _platform = platform,
32 _processManager = processManager,
33 _processUtils = ProcessUtils(processManager: processManager, logger: logger);
34
Andrew Kolos06e2b1812023-06-01 04:19:19 +000035 /// Within the Java ecosystem, this environment variable is typically set
36 /// the install location of a Java Runtime Environment (JRE) or Java
37 /// Development Kit (JDK).
38 ///
39 /// Tools that depend on Java and need to find it will often check this
40 /// variable. If you are looking to set `JAVA_HOME` when stating a process,
41 /// consider using the [environment] instance property instead.
42 static String javaHomeEnvironmentVariable = 'JAVA_HOME';
43
Andrew Kolos80a4f9b2023-05-20 06:29:21 +000044 /// Finds the Java runtime environment that should be used for all java-dependent
45 /// operations across the tool.
46 ///
47 /// This searches for Java in the following places, in order:
48 ///
49 /// 1. the runtime environment bundled with Android Studio;
50 /// 2. the runtime environment found in the JAVA_HOME env variable, if set; or
51 /// 3. the java binary found on PATH.
52 ///
53 /// Returns null if no java binary could be found.
Andrew Kolos80a4f9b2023-05-20 06:29:21 +000054 static Java? find({
Andrew Kolos96afa502023-06-07 04:52:17 +000055 required Config config,
Andrew Kolos80a4f9b2023-05-20 06:29:21 +000056 required AndroidStudio? androidStudio,
57 required Logger logger,
58 required FileSystem fileSystem,
59 required Platform platform,
60 required ProcessManager processManager,
61 }) {
62 final OperatingSystemUtils os = OperatingSystemUtils(
63 fileSystem: fileSystem,
64 logger: logger,
65 platform: platform,
66 processManager: processManager
67 );
68 final String? home = _findJavaHome(
Andrew Kolos96afa502023-06-07 04:52:17 +000069 config: config,
Andrew Kolos80a4f9b2023-05-20 06:29:21 +000070 logger: logger,
71 androidStudio: androidStudio,
72 platform: platform
73 );
74 final String? binary = _findJavaBinary(
75 logger: logger,
76 javaHome: home,
77 fileSystem: fileSystem,
78 operatingSystemUtils: os,
79 platform: platform
80 );
81
82 if (binary == null) {
83 return null;
84 }
85
86 return Java(
87 javaHome: home,
88 binaryPath: binary,
89 logger: logger,
90 fileSystem: fileSystem,
91 os: os,
92 platform: platform,
93 processManager: processManager,
94 );
95 }
96
Andrew Kolos06e2b1812023-06-01 04:19:19 +000097 /// The path of the runtime environments' home directory.
Andrew Kolos80a4f9b2023-05-20 06:29:21 +000098 ///
99 /// This should only be used for logging and validation purposes.
100 /// If you need to set JAVA_HOME when starting a process, consider
101 /// using [environment] instead.
102 /// If you need to inspect the files of the runtime, considering adding
103 /// a new method to this class instead.
104 final String? javaHome;
105
Andrew Kolos06e2b1812023-06-01 04:19:19 +0000106 /// The path of the runtime environments' java binary.
Andrew Kolos80a4f9b2023-05-20 06:29:21 +0000107 ///
108 /// This should be only used for logging and validation purposes.
109 /// If you need to invoke the binary directly, consider adding a new method
110 /// to this class instead.
111 final String binaryPath;
112
113 final Logger _logger;
114 final FileSystem _fileSystem;
115 final OperatingSystemUtils _os;
116 final Platform _platform;
117 final ProcessManager _processManager;
118 final ProcessUtils _processUtils;
119
120 /// Returns an environment variable map with
121 /// 1. JAVA_HOME set if this object has a known home directory, and
122 /// 2. The java binary folder appended onto PATH, if the binary location is known.
123 ///
124 /// This map should be used as the environment when invoking any Java-dependent
125 /// processes, such as Gradle or Android SDK tools (avdmanager, sdkmanager, etc.)
126 Map<String, String> get environment {
127 return <String, String>{
Andrew Kolos06e2b1812023-06-01 04:19:19 +0000128 if (javaHome != null) javaHomeEnvironmentVariable: javaHome!,
Andrew Kolos80a4f9b2023-05-20 06:29:21 +0000129 'PATH': _fileSystem.path.dirname(binaryPath) +
130 _os.pathVarSeparator +
131 _platform.environment['PATH']!,
132 };
133 }
134
135 /// Returns the version of java in the format \d(.\d)+(.\d)+
136 /// Returns null if version could not be determined.
Andrew Kolos06e2b1812023-06-01 04:19:19 +0000137 late final Version? version = (() {
Andrew Kolosc22c19b2023-12-06 15:13:17 -0800138 if (!canRun()) {
139 return null;
140 }
141
Andrew Kolos80a4f9b2023-05-20 06:29:21 +0000142 final RunResult result = _processUtils.runSync(
143 <String>[binaryPath, '--version'],
144 environment: environment,
145 );
146 if (result.exitCode != 0) {
147 _logger.printTrace('java --version failed: exitCode: ${result.exitCode}'
148 ' stdout: ${result.stdout} stderr: ${result.stderr}');
Andrew Kolosc22c19b2023-12-06 15:13:17 -0800149 return null;
Andrew Kolos80a4f9b2023-05-20 06:29:21 +0000150 }
Andrew Kolos06e2b1812023-06-01 04:19:19 +0000151 final String rawVersionOutput = result.stdout;
152 final List<String> versionLines = rawVersionOutput.split('\n');
153 // Should look something like 'openjdk 19.0.2 2023-01-17'.
154 final String longVersionText = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
155
Reid Baker311193d2023-11-27 11:14:58 -0500156 // The contents that matter come in the format '11.0.18', '1.8.0_202 or 21'.
157 final RegExp jdkVersionRegex = RegExp(r'(?<version>\d+(\.\d+(\.\d+(?:_\d+)?)?)?)');
Andrew Kolos06e2b1812023-06-01 04:19:19 +0000158 final Iterable<RegExpMatch> matches =
159 jdkVersionRegex.allMatches(rawVersionOutput);
160 if (matches.isEmpty) {
Reid Baker94079b92023-11-09 13:38:18 -0500161 // Fallback to second string format like "java 21.0.1 2023-09-19 LTS"
162 final RegExp secondJdkVersionRegex =
163 RegExp(r'java\s+(?<version>\d+(\.\d+)?(\.\d+)?)\s+\d\d\d\d-\d\d-\d\d');
164 final RegExpMatch? match = secondJdkVersionRegex.firstMatch(versionLines[0]);
165 if (match != null) {
166 final Version? parsedVersion = Version.parse(match.namedGroup('version'));
167 if (parsedVersion == null) {
168 return null;
169 }
170 return parsedVersion;
171 }
Andrew Kolos06e2b1812023-06-01 04:19:19 +0000172 _logger.printWarning(_formatJavaVersionWarning(rawVersionOutput));
173 return null;
174 }
Reid Baker311193d2023-11-27 11:14:58 -0500175 final String? version = matches.first.namedGroup('version');
Andrew Kolos06e2b1812023-06-01 04:19:19 +0000176 if (version == null || version.split('_').isEmpty) {
177 _logger.printWarning(_formatJavaVersionWarning(rawVersionOutput));
178 return null;
179 }
180
181 // Trim away _d+ from versions 1.8 and below.
182 final String versionWithoutBuildInfo = version.split('_').first;
183
184 final Version? parsedVersion = Version.parse(versionWithoutBuildInfo);
185 if (parsedVersion == null) {
186 return null;
187 }
188 return Version.withText(
189 parsedVersion.major,
190 parsedVersion.minor,
191 parsedVersion.patch,
192 longVersionText,
193 );
Andrew Kolos80a4f9b2023-05-20 06:29:21 +0000194 })();
195
196 bool canRun() {
197 return _processManager.canRun(binaryPath);
198 }
199}
200
201String? _findJavaHome({
Andrew Kolos96afa502023-06-07 04:52:17 +0000202 required Config config,
Andrew Kolos80a4f9b2023-05-20 06:29:21 +0000203 required Logger logger,
204 required AndroidStudio? androidStudio,
205 required Platform platform,
206}) {
Andrew Kolos96afa502023-06-07 04:52:17 +0000207 final Object? configured = config.getValue('jdk-dir');
208 if (configured != null) {
209 return configured as String;
210 }
211
Andrew Kolos80a4f9b2023-05-20 06:29:21 +0000212 final String? androidStudioJavaPath = androidStudio?.javaPath;
213 if (androidStudioJavaPath != null) {
214 return androidStudioJavaPath;
215 }
216
Andrew Kolos06e2b1812023-06-01 04:19:19 +0000217 final String? javaHomeEnv = platform.environment[Java.javaHomeEnvironmentVariable];
Andrew Kolos80a4f9b2023-05-20 06:29:21 +0000218 if (javaHomeEnv != null) {
219 return javaHomeEnv;
220 }
221 return null;
222}
223
224String? _findJavaBinary({
225 required Logger logger,
226 required String? javaHome,
227 required FileSystem fileSystem,
228 required OperatingSystemUtils operatingSystemUtils,
229 required Platform platform,
230}) {
231 if (javaHome != null) {
232 return fileSystem.path.join(javaHome, 'bin', 'java');
233 }
234
235 // Fallback to PATH based lookup.
Andrew Kolos06e2b1812023-06-01 04:19:19 +0000236 return operatingSystemUtils.which(_javaExecutable)?.path;
Andrew Kolos80a4f9b2023-05-20 06:29:21 +0000237}
238
239// Returns a user visible String that says the tool failed to parse
240// the version of java along with the output.
241String _formatJavaVersionWarning(String javaVersionRaw) {
242 return 'Could not parse java version from: \n'
243 '$javaVersionRaw \n'
244 'If there is a version please look for an existing bug '
245 'https://github.com/flutter/flutter/issues/ '
246 'and if one does not exist file a new issue.';
247}