Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 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 | |
Andrew Kolos | 96afa50 | 2023-06-07 04:52:17 +0000 | [diff] [blame] | 7 | import '../base/config.dart'; |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 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'; |
Andrew Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 13 | import '../base/version.dart'; |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 14 | import 'android_studio.dart'; |
| 15 | |
Andrew Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 16 | const String _javaExecutable = 'java'; |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 17 | |
| 18 | /// Represents an installation of Java. |
| 19 | class 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 Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 35 | /// 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 Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 44 | /// 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 Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 54 | static Java? find({ |
Andrew Kolos | 96afa50 | 2023-06-07 04:52:17 +0000 | [diff] [blame] | 55 | required Config config, |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 56 | 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 Kolos | 96afa50 | 2023-06-07 04:52:17 +0000 | [diff] [blame] | 69 | config: config, |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 70 | 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 Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 97 | /// The path of the runtime environments' home directory. |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 98 | /// |
| 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 Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 106 | /// The path of the runtime environments' java binary. |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 107 | /// |
| 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 Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 128 | if (javaHome != null) javaHomeEnvironmentVariable: javaHome!, |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 129 | '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 Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 137 | late final Version? version = (() { |
Andrew Kolos | c22c19b | 2023-12-06 15:13:17 -0800 | [diff] [blame] | 138 | if (!canRun()) { |
| 139 | return null; |
| 140 | } |
| 141 | |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 142 | 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 Kolos | c22c19b | 2023-12-06 15:13:17 -0800 | [diff] [blame] | 149 | return null; |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 150 | } |
Andrew Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 151 | 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 Baker | 311193d | 2023-11-27 11:14:58 -0500 | [diff] [blame] | 156 | // 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 Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 158 | final Iterable<RegExpMatch> matches = |
| 159 | jdkVersionRegex.allMatches(rawVersionOutput); |
| 160 | if (matches.isEmpty) { |
Reid Baker | 94079b9 | 2023-11-09 13:38:18 -0500 | [diff] [blame] | 161 | // 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 Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 172 | _logger.printWarning(_formatJavaVersionWarning(rawVersionOutput)); |
| 173 | return null; |
| 174 | } |
Reid Baker | 311193d | 2023-11-27 11:14:58 -0500 | [diff] [blame] | 175 | final String? version = matches.first.namedGroup('version'); |
Andrew Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 176 | 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 Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 194 | })(); |
| 195 | |
| 196 | bool canRun() { |
| 197 | return _processManager.canRun(binaryPath); |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | String? _findJavaHome({ |
Andrew Kolos | 96afa50 | 2023-06-07 04:52:17 +0000 | [diff] [blame] | 202 | required Config config, |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 203 | required Logger logger, |
| 204 | required AndroidStudio? androidStudio, |
| 205 | required Platform platform, |
| 206 | }) { |
Andrew Kolos | 96afa50 | 2023-06-07 04:52:17 +0000 | [diff] [blame] | 207 | final Object? configured = config.getValue('jdk-dir'); |
| 208 | if (configured != null) { |
| 209 | return configured as String; |
| 210 | } |
| 211 | |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 212 | final String? androidStudioJavaPath = androidStudio?.javaPath; |
| 213 | if (androidStudioJavaPath != null) { |
| 214 | return androidStudioJavaPath; |
| 215 | } |
| 216 | |
Andrew Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 217 | final String? javaHomeEnv = platform.environment[Java.javaHomeEnvironmentVariable]; |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 218 | if (javaHomeEnv != null) { |
| 219 | return javaHomeEnv; |
| 220 | } |
| 221 | return null; |
| 222 | } |
| 223 | |
| 224 | String? _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 Kolos | 06e2b181 | 2023-06-01 04:19:19 +0000 | [diff] [blame] | 236 | return operatingSystemUtils.which(_javaExecutable)?.path; |
Andrew Kolos | 80a4f9b | 2023-05-20 06:29:21 +0000 | [diff] [blame] | 237 | } |
| 238 | |
| 239 | // Returns a user visible String that says the tool failed to parse |
| 240 | // the version of java along with the output. |
| 241 | String _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 | } |