From 6273cd5aedbf1f8f870bba7db81fa37759dbd144 Mon Sep 17 00:00:00 2001 From: Fabian Meumertzheim Date: Sat, 16 Sep 2023 08:06:20 +0200 Subject: [PATCH] Diagnose incompatible Java system classpaths When the Java system classpath extracted from the target Java runtime is more recent than the Java runtime used for Java compilation, JavaBuilder now emits a `[BazelJavaConfiguration]` diagnostic with actionable information instead of a bunch of "bad class file" errors on `module-info` files. Example: ``` error: [BazelJavaConfiguration] The Java 17 runtime used to run javac is not recent enough to compile for the Java 20 runtime in external/remotejdk20_linux. Either register a Java toolchain with a newer java_runtime or specify a lower --java_runtime_version. ``` --- .../build/buildjar/javac/BlazeJavacMain.java | 37 ++++++++ src/test/shell/bazel/bazel_java17_test.sh | 88 +++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/BlazeJavacMain.java b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/BlazeJavacMain.java index 6a24ddb291d024..4fe2c1f6b444a8 100644 --- a/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/BlazeJavacMain.java +++ b/src/java_tools/buildjar/java/com/google/devtools/build/buildjar/javac/BlazeJavacMain.java @@ -19,6 +19,7 @@ import static com.google.common.collect.MoreCollectors.toOptional; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Comparator.comparing; +import static java.util.Locale.ENGLISH; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -72,6 +73,9 @@ */ public class BlazeJavacMain { + private static final Pattern INCOMPATIBLE_SYSTEM_CLASS_PATH_ERROR = + Pattern.compile( + "(?s)bad class file: /modules/.*class file has wrong version (?[4-9][0-9])\\."); private static final Pattern UNSUPPORTED_CLASS_VERSION_ERROR = Pattern.compile( "^(?[^ ]*) has been compiled by a more recent version of the Java Runtime " @@ -183,6 +187,12 @@ public static BlazeJavacResult compile(BlazeJavacArguments arguments) { errWriter.flush(); ImmutableList diagnostics = diagnosticsBuilder.build(); + diagnostics.stream() + .map(d -> maybeGetJavaConfigurationError(arguments, d)) + .flatMap(Optional::stream) + .findFirst() + .ifPresent(errOutput::append); + boolean werror = diagnostics.stream().anyMatch(d -> d.getCode().equals("compiler.err.warnings.and.werror")); if (status.equals(Status.OK)) { @@ -279,6 +289,33 @@ private static boolean shouldReportDiagnostic(boolean werror, FormattedDiagnosti return false; } + private static Optional maybeGetJavaConfigurationError( + BlazeJavacArguments arguments, Diagnostic diagnostic) { + if (!diagnostic.getKind().equals(Diagnostic.Kind.ERROR)) { + return Optional.empty(); + } + Matcher matcher; + if (!diagnostic.getCode().equals("compiler.err.cant.access") + || arguments.system() == null + || !(matcher = INCOMPATIBLE_SYSTEM_CLASS_PATH_ERROR.matcher(diagnostic.getMessage(ENGLISH))) + .find()) { + return Optional.empty(); + } + // The output path is of the form $PRODUCT-out/$CPU-$MODE[-exec-...]/bin/... + boolean isForTool = arguments.classOutput().subpath(1, 2).toString().contains("-exec-"); + // Java 8 corresponds to class file major version 52. + int systemClasspathVersion = Integer.parseUnsignedInt(matcher.group("version")) - 44; + return Optional.of( + String.format( + "error: [BazelJavaConfiguration] The Java %d runtime used to run javac is not recent " + + "enough to compile for the Java %d runtime in %s. Either register a Java " + + "toolchain with a newer java_runtime or specify a lower %s.\n", + Runtime.version().feature(), + systemClasspathVersion, + arguments.system(), + isForTool ? "--tool_java_runtime_version" : "--java_runtime_version")); + } + /** Processes Plugin-specific arguments and removes them from the args array. */ @VisibleForTesting static void processPluginArgs( diff --git a/src/test/shell/bazel/bazel_java17_test.sh b/src/test/shell/bazel/bazel_java17_test.sh index 093546686c0ba7..6b44ee09bf5a86 100755 --- a/src/test/shell/bazel/bazel_java17_test.sh +++ b/src/test/shell/bazel/bazel_java17_test.sh @@ -186,4 +186,92 @@ EOF "--tool_java_language_version\." } +function test_incompatible_system_classpath() { + mkdir -p pkg + # This test defines a custom Java toolchain as it relies on the availability of a runtime that is + # strictly newer than the one specified as the toolchain's java_runtime. + cat >pkg/BUILD <<'EOF' +load("@bazel_tools//tools/jdk:default_java_toolchain.bzl", "default_java_toolchain") +java_binary( + name = "Main", + srcs = ["Main.java"], + main_class = "com.example.Main", +) +default_java_toolchain( + name = "java_toolchain", + source_version = "17", + target_version = "17", + java_runtime = "@bazel_tools//tools/jdk:remotejdk_17", +) +EOF + + cat >pkg/Main.java <<'EOF' +package com.example; +import java.net.URI; +public class Main { + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} +EOF + + bazel build //pkg:Main \ + --extra_toolchains=//pkg:java_toolchain_definition \ + --java_language_version=17 \ + --java_runtime_version=remotejdk_20 \ + &>"${TEST_log}" && fail "Expected build to fail" + + expect_log "error: \[BazelJavaConfiguration\] The Java 17 runtime used to run javac is not " \ + "recent enough to compile for the Java 20 runtime in external/remotejdk20_[a-z0-9]*\. Either " \ + "register a Java toolchain with a newer java_runtime or specify a lower " \ + "--java_runtime_version\." +} + +function test_incompatible_tool_system_classpath() { + mkdir -p pkg + # This test defines a custom Java toolchain as it relies on the availability of a runtime that is + # strictly newer than the one specified as the toolchain's java_runtime. + cat >pkg/BUILD <<'EOF' +load("@bazel_tools//tools/jdk:default_java_toolchain.bzl", "default_java_toolchain") +java_binary( + name = "Main", + srcs = ["Main.java"], + main_class = "com.example.Main", +) +genrule( + name = "gen", + outs = ["gen.txt"], + tools = [":Main"], + cmd = "$(location :Main) > $@", +) +default_java_toolchain( + name = "java_toolchain", + source_version = "17", + target_version = "17", + java_runtime = "@bazel_tools//tools/jdk:remotejdk_17", +) +EOF + + cat >pkg/Main.java <<'EOF' +package com.example; +import java.net.URI; +public class Main { + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} +EOF + + bazel build //pkg:gen \ + --extra_toolchains=//pkg:java_toolchain_definition \ + --tool_java_language_version=17 \ + --tool_java_runtime_version=remotejdk_20 \ + &>"${TEST_log}" && fail "Expected build to fail" + + expect_log "error: \[BazelJavaConfiguration\] The Java 17 runtime used to run javac is not " \ + "recent enough to compile for the Java 20 runtime in external/remotejdk20_[a-z0-9]*\. Either " \ + "register a Java toolchain with a newer java_runtime or specify a lower " \ + "--tool_java_runtime_version\." +} + run_suite "Tests Java 17 language features"