Andrew Kolos c22c19b6e2
have Java.version return null if java --version fails or cannot be run (#139614)
## Summary
Fixes https://github.com/flutter/flutter/issues/139180, where `flutter create` could crash if the `java` binary the tool found cannot be run.

## Context

At startup, the tool searches for a Java installation[^1]. Unless the located installation is from [an Android Studio installation](e1967ecabf/packages/flutter_tools/lib/src/android/android_studio.dart (L163)), the tool does not verify that the binary is runnable. For more, see https://github.com/flutter/flutter/issues/139613, which tracks this inconsistency in behavior. 

This means that in the scenario where

1) the user does not have Android Studio installed or the java binary found within cannot be run **and**
2) the user has a) `flutter config --jdk-dir` set, b) `JAVA_HOME` set in their environment, **or** c) `java` on their system path **and**
3) the java binary we think we found during cannot be run (or `java --version` fails), **then**

the user running `flutter create` with Android enabled will hit a tool crash.

## Change

`Java.version` should return null if version checking fails for any reason. [This is documented behavior](48f57621ad/packages/flutter_tools/lib/src/android/java.dart (L136)). Therefore, we'll update the implementation to first verify that the binary is runnable. If it isn't, it will return `null`.

[^1]: We find java by calling the static `Java.find`, see: 48187028c1/packages/flutter_tools/lib/src/context_runner.dart (L271)
[^2]: This PR doesn't change this, as this would be too dangerous to cherry-pick into stable.
2023-12-06 23:13:17 +00:00

248 lines
8.0 KiB
Dart

// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:process/process.dart';
import '../base/config.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/process.dart';
import '../base/version.dart';
import 'android_studio.dart';
const String _javaExecutable = 'java';
/// Represents an installation of Java.
class Java {
Java({
required this.javaHome,
required this.binaryPath,
required Logger logger,
required FileSystem fileSystem,
required OperatingSystemUtils os,
required Platform platform,
required ProcessManager processManager,
}): _logger = logger,
_fileSystem = fileSystem,
_os = os,
_platform = platform,
_processManager = processManager,
_processUtils = ProcessUtils(processManager: processManager, logger: logger);
/// Within the Java ecosystem, this environment variable is typically set
/// the install location of a Java Runtime Environment (JRE) or Java
/// Development Kit (JDK).
///
/// Tools that depend on Java and need to find it will often check this
/// variable. If you are looking to set `JAVA_HOME` when stating a process,
/// consider using the [environment] instance property instead.
static String javaHomeEnvironmentVariable = 'JAVA_HOME';
/// Finds the Java runtime environment that should be used for all java-dependent
/// operations across the tool.
///
/// This searches for Java in the following places, in order:
///
/// 1. the runtime environment bundled with Android Studio;
/// 2. the runtime environment found in the JAVA_HOME env variable, if set; or
/// 3. the java binary found on PATH.
///
/// Returns null if no java binary could be found.
static Java? find({
required Config config,
required AndroidStudio? androidStudio,
required Logger logger,
required FileSystem fileSystem,
required Platform platform,
required ProcessManager processManager,
}) {
final OperatingSystemUtils os = OperatingSystemUtils(
fileSystem: fileSystem,
logger: logger,
platform: platform,
processManager: processManager
);
final String? home = _findJavaHome(
config: config,
logger: logger,
androidStudio: androidStudio,
platform: platform
);
final String? binary = _findJavaBinary(
logger: logger,
javaHome: home,
fileSystem: fileSystem,
operatingSystemUtils: os,
platform: platform
);
if (binary == null) {
return null;
}
return Java(
javaHome: home,
binaryPath: binary,
logger: logger,
fileSystem: fileSystem,
os: os,
platform: platform,
processManager: processManager,
);
}
/// The path of the runtime environments' home directory.
///
/// This should only be used for logging and validation purposes.
/// If you need to set JAVA_HOME when starting a process, consider
/// using [environment] instead.
/// If you need to inspect the files of the runtime, considering adding
/// a new method to this class instead.
final String? javaHome;
/// The path of the runtime environments' java binary.
///
/// This should be only used for logging and validation purposes.
/// If you need to invoke the binary directly, consider adding a new method
/// to this class instead.
final String binaryPath;
final Logger _logger;
final FileSystem _fileSystem;
final OperatingSystemUtils _os;
final Platform _platform;
final ProcessManager _processManager;
final ProcessUtils _processUtils;
/// Returns an environment variable map with
/// 1. JAVA_HOME set if this object has a known home directory, and
/// 2. The java binary folder appended onto PATH, if the binary location is known.
///
/// This map should be used as the environment when invoking any Java-dependent
/// processes, such as Gradle or Android SDK tools (avdmanager, sdkmanager, etc.)
Map<String, String> get environment {
return <String, String>{
if (javaHome != null) javaHomeEnvironmentVariable: javaHome!,
'PATH': _fileSystem.path.dirname(binaryPath) +
_os.pathVarSeparator +
_platform.environment['PATH']!,
};
}
/// Returns the version of java in the format \d(.\d)+(.\d)+
/// Returns null if version could not be determined.
late final Version? version = (() {
if (!canRun()) {
return null;
}
final RunResult result = _processUtils.runSync(
<String>[binaryPath, '--version'],
environment: environment,
);
if (result.exitCode != 0) {
_logger.printTrace('java --version failed: exitCode: ${result.exitCode}'
' stdout: ${result.stdout} stderr: ${result.stderr}');
return null;
}
final String rawVersionOutput = result.stdout;
final List<String> versionLines = rawVersionOutput.split('\n');
// Should look something like 'openjdk 19.0.2 2023-01-17'.
final String longVersionText = versionLines.length >= 2 ? versionLines[1] : versionLines[0];
// The contents that matter come in the format '11.0.18', '1.8.0_202 or 21'.
final RegExp jdkVersionRegex = RegExp(r'(?<version>\d+(\.\d+(\.\d+(?:_\d+)?)?)?)');
final Iterable<RegExpMatch> matches =
jdkVersionRegex.allMatches(rawVersionOutput);
if (matches.isEmpty) {
// Fallback to second string format like "java 21.0.1 2023-09-19 LTS"
final RegExp secondJdkVersionRegex =
RegExp(r'java\s+(?<version>\d+(\.\d+)?(\.\d+)?)\s+\d\d\d\d-\d\d-\d\d');
final RegExpMatch? match = secondJdkVersionRegex.firstMatch(versionLines[0]);
if (match != null) {
final Version? parsedVersion = Version.parse(match.namedGroup('version'));
if (parsedVersion == null) {
return null;
}
return parsedVersion;
}
_logger.printWarning(_formatJavaVersionWarning(rawVersionOutput));
return null;
}
final String? version = matches.first.namedGroup('version');
if (version == null || version.split('_').isEmpty) {
_logger.printWarning(_formatJavaVersionWarning(rawVersionOutput));
return null;
}
// Trim away _d+ from versions 1.8 and below.
final String versionWithoutBuildInfo = version.split('_').first;
final Version? parsedVersion = Version.parse(versionWithoutBuildInfo);
if (parsedVersion == null) {
return null;
}
return Version.withText(
parsedVersion.major,
parsedVersion.minor,
parsedVersion.patch,
longVersionText,
);
})();
bool canRun() {
return _processManager.canRun(binaryPath);
}
}
String? _findJavaHome({
required Config config,
required Logger logger,
required AndroidStudio? androidStudio,
required Platform platform,
}) {
final Object? configured = config.getValue('jdk-dir');
if (configured != null) {
return configured as String;
}
final String? androidStudioJavaPath = androidStudio?.javaPath;
if (androidStudioJavaPath != null) {
return androidStudioJavaPath;
}
final String? javaHomeEnv = platform.environment[Java.javaHomeEnvironmentVariable];
if (javaHomeEnv != null) {
return javaHomeEnv;
}
return null;
}
String? _findJavaBinary({
required Logger logger,
required String? javaHome,
required FileSystem fileSystem,
required OperatingSystemUtils operatingSystemUtils,
required Platform platform,
}) {
if (javaHome != null) {
return fileSystem.path.join(javaHome, 'bin', 'java');
}
// Fallback to PATH based lookup.
return operatingSystemUtils.which(_javaExecutable)?.path;
}
// Returns a user visible String that says the tool failed to parse
// the version of java along with the output.
String _formatJavaVersionWarning(String javaVersionRaw) {
return 'Could not parse java version from: \n'
'$javaVersionRaw \n'
'If there is a version please look for an existing bug '
'https://github.com/flutter/flutter/issues/ '
'and if one does not exist file a new issue.';
}