From 67bc554792c144699e6b256873fb492b80c3e8d7 Mon Sep 17 00:00:00 2001 From: Eugene Zhuravlev Date: Mon, 11 Aug 2025 18:57:08 +0200 Subject: [PATCH] Provide a convenient way to configure 'fallback JDK' to compile projects using older JVM targets, e.g. java 7 (IDEA-370407) this commit also changes the policy of choosing the fallback JDK for the forked javac, which is now the oldest available in the JDK table, but not older than the minimal supported JDK version for running javac GitOrigin-RevId: 1c757fc1faa0aff36ecac04b8d12a8a2f7e313a6 --- .../intellij/compiler/impl/CompileDriver.java | 63 ++++++++++---- .../compiler/server/BuildManager.java | 83 +++++++++---------- .../messages/JpsBuildBundle.properties | 9 ++ .../org/jetbrains/jps/api/GlobalOptions.java | 4 + .../jps/incremental/java/JavaBuilder.java | 54 ++++++++---- .../FallbackJdkSetupNotification.java | 17 ++++ 6 files changed, 156 insertions(+), 74 deletions(-) create mode 100644 jps/jps-builders/src/org/jetbrains/jps/incremental/messages/FallbackJdkSetupNotification.java diff --git a/java/compiler/impl/src/com/intellij/compiler/impl/CompileDriver.java b/java/compiler/impl/src/com/intellij/compiler/impl/CompileDriver.java index 712b3827ca6a..f6685255f1d5 100644 --- a/java/compiler/impl/src/com/intellij/compiler/impl/CompileDriver.java +++ b/java/compiler/impl/src/com/intellij/compiler/impl/CompileDriver.java @@ -10,8 +10,8 @@ import com.intellij.compiler.server.BuildManager; import com.intellij.compiler.server.DefaultMessageHandler; import com.intellij.ide.nls.NlsMessages; import com.intellij.internal.statistic.StructuredIdeActivity; -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationListener; +import com.intellij.java.JavaBundle; +import com.intellij.notification.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.application.ReadAction; @@ -25,17 +25,14 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectBundle; import com.intellij.openapi.projectRoots.Sdk; -import com.intellij.openapi.roots.CompilerModuleExtension; -import com.intellij.openapi.roots.ModuleRootManager; -import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.roots.*; import com.intellij.openapi.roots.ui.configuration.DefaultModuleConfigurationEditorFactory; +import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService; import com.intellij.openapi.ui.MessageType; import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.util.Key; -import com.intellij.openapi.util.NlsContexts; -import com.intellij.openapi.util.Pair; -import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.*; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.HtmlChunk; @@ -69,6 +66,7 @@ import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import static org.jetbrains.jps.api.CmdlineRemoteProto.Message.ControllerMessage.ParametersMessage.TargetTypeBuildScope; @@ -355,6 +353,9 @@ public final class CompileDriver { } } + + private final AtomicBoolean myFallbackSdkHintReported = new AtomicBoolean(false); + @Override protected void handleBuildEvent(UUID sessionId, CmdlineRemoteProto.Message.BuilderMessage.BuildEvent event) { final CmdlineRemoteProto.Message.BuilderMessage.BuildEvent.Type eventType = event.getEventType(); @@ -398,12 +399,32 @@ public final class CompileDriver { if (event.hasCustomBuilderMessage()) { final CmdlineRemoteProto.Message.BuilderMessage.BuildEvent.CustomBuilderMessage message = event.getCustomBuilderMessage(); - if (GlobalOptions.JPS_SYSTEM_BUILDER_ID.equals(message.getBuilderId()) && - GlobalOptions.JPS_UNPROCESSED_FS_CHANGES_MESSAGE_ID.equals(message.getMessageType())) { - //noinspection HardCodedStringLiteral - final String text = message.getMessageText(); - if (!StringUtil.isEmpty(text)) { - compileContext.addMessage(CompilerMessageCategory.INFORMATION, text, null, -1, -1); + if (GlobalOptions.JPS_SYSTEM_BUILDER_ID.equals(message.getBuilderId())) { + if (GlobalOptions.JPS_UNPROCESSED_FS_CHANGES_MESSAGE_ID.equals(message.getMessageType())) { + //noinspection HardCodedStringLiteral + final String text = message.getMessageText(); + if (!StringUtil.isEmpty(text)) { + compileContext.addMessage(CompilerMessageCategory.INFORMATION, text, null, -1, -1); + } + } + else if (GlobalOptions.JPS_FALLBACK_SDK_SETUP_MESSAGE_ID.equals(message.getMessageType())) { + if (!myFallbackSdkHintReported.getAndSet(true)) { + @NlsSafe String notificationContent = message.getMessageText(); + NotificationGroup notificationGroup = NotificationGroupManager.getInstance().getNotificationGroup("Unsupported JDK"); + + Notification notification = notificationGroup.createNotification( + JavaBundle.message("unsupported.jdk.notification.title"), + notificationContent, + NotificationType.WARNING + ) + .setImportantSuggestion(true) + .setRemoveWhenExpired(true) + .addAction( + NotificationAction.createSimpleExpiring(ProjectBundle.message("action.text.config.invalid.sdk.configure"), () -> openJdkConfigurationSettings(compileContext.getCompileScope())) + ); + compileContext.getBuildSession().registerCloseAction(notification::expire); + notification.notify(myProject); + } } } } @@ -418,6 +439,18 @@ public final class CompileDriver { }); } + private void openJdkConfigurationSettings(CompileScope compileScope) { + for (Module module : compileScope.getAffectedModules()) { + for (OrderEntry entry : ModuleRootManager.getInstance(module).getOrderEntries()) { + if (entry instanceof JdkOrderEntry) { + ProjectSettingsService.getInstance(myProject).openLibraryOrSdkSettings(entry); + return; + } + } + } + ProjectSettingsService.getInstance(myProject).openProjectSettings(); + } + @RequiresEdt private void startup(final CompileScope scope, final boolean isRebuild, diff --git a/java/compiler/impl/src/com/intellij/compiler/server/BuildManager.java b/java/compiler/impl/src/com/intellij/compiler/server/BuildManager.java index 1944c50daf45..fad8293487f5 100644 --- a/java/compiler/impl/src/com/intellij/compiler/server/BuildManager.java +++ b/java/compiler/impl/src/com/intellij/compiler/server/BuildManager.java @@ -63,7 +63,6 @@ import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.startup.StartupActivity; import com.intellij.openapi.util.*; import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.registry.RegistryManager; import com.intellij.openapi.util.registry.RegistryManagerKt; @@ -187,9 +186,6 @@ public final class BuildManager implements Disposable { private static final String JPS_USE_EXPERIMENTAL_STORAGE = "jps.use.experimental.storage"; - private final String myFallbackSdkHome; - private final String myFallbackSdkVersion; - private final Map, Project> myAutomakeFutures = Collections.synchronizedMap(new HashMap<>()); private final Map> myBuildsInProgress = Collections.synchronizedMap(new HashMap<>()); private final Map, OSProcessHandler>>> myPreloadedBuilds = @@ -325,19 +321,6 @@ public final class BuildManager implements Disposable { final Application application = ApplicationManager.getApplication(); IS_UNIT_TEST_MODE = application.isUnitTestMode(); - - String fallbackSdkHome = System.getProperty(GlobalOptions.FALLBACK_JDK_HOME, null); - String fallbackSdkVersion = System.getProperty(GlobalOptions.FALLBACK_JDK_VERSION, null); - if (fallbackSdkHome == null || fallbackSdkVersion == null) { - // default to the IDE's runtime - myFallbackSdkHome = getFallbackSdkHome(); - myFallbackSdkVersion = SystemInfo.JAVA_VERSION; - } - else { - myFallbackSdkHome = fallbackSdkHome; - myFallbackSdkVersion = fallbackSdkVersion; - } - SimpleMessageBusConnection connection = application.getMessageBus().connect(coroutineScope); connection.subscribe(ProjectCloseListener.TOPIC, new ProjectWatcher()); connection.subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener() { @@ -495,17 +478,6 @@ public final class BuildManager implements Disposable { } } - private static @NotNull String getFallbackSdkHome() { - String home = SystemProperties.getJavaHome(); // should point either to jre or jdk - if (!JdkUtil.checkForJdk(home)) { - String parent = new File(home).getParent(); - if (parent != null && JdkUtil.checkForJdk(parent)) { - home = parent; - } - } - return FileUtilRt.toSystemIndependentName(home); - } - private static @NotNull List getOpenProjects() { final Project[] projects = ProjectManager.getInstance().getOpenProjects(); if (projects.length == 0) { @@ -1210,19 +1182,11 @@ public final class BuildManager implements Disposable { return getRuntimeSdk(project, MINIMUM_REQUIRED_JPS_BUILD_JAVA_VERSION, processed -> { // build's fallback SDK choosing policy: select among unprocessed SDKs in the SDK table the oldest possible one that can be used - // select only SDKs that match project's WSL VM, if any - Supplier projectWslDistribution = new Supplier<>() { - private WSLDistribution val; - - @Override - public WSLDistribution get() { - return val != null ? val : (val = findWSLDistribution(project)); - } - }; - Predicate sdkFilter = sdk -> !processed.contains(sdk) && Objects.equals(projectWslDistribution.get(), findWSLDistribution(sdk)); + // if project is located in a WSL VM, consider only SDKs, configured in the same WSL VM + Predicate wslSdkFilter = getWslSdkFilter(project); return StreamEx.of(ProjectJdkTable.getInstance().getSdksOfType(JavaSdk.getInstance())) - .filter(sdkFilter) + .filter(sdk -> !processed.contains(sdk) && wslSdkFilter.test(sdk)) .mapToEntry(sdk -> JavaVersion.tryParse(sdk.getVersionString())) .filterValues(version -> version != null && version.isAtLeast(MINIMUM_REQUIRED_JPS_BUILD_JAVA_VERSION)) .min(Map.Entry.comparingByValue()) @@ -1232,6 +1196,19 @@ public final class BuildManager implements Disposable { }); } + private static @NotNull Predicate getWslSdkFilter(@NotNull Project project) { + // if WSL is configured, accepts only those SDKs that match project's WSL VM + Supplier projectWslDistribution = new Supplier<>() { + private Ref val; + + @Override + public WSLDistribution get() { + return val != null? val.get() : (val = Ref.create(findWSLDistribution(project))).get(); + } + }; + return sdk -> Objects.equals(projectWslDistribution.get(), findWSLDistribution(sdk)); + } + public static @NotNull Pair<@NotNull Sdk, @Nullable JavaSdkVersion> getJavacRuntimeSdk(@NotNull Project project) { return getRuntimeSdk(project, ExternalJavacProcess.MINIMUM_REQUIRED_JAVA_VERSION, processed -> getIDERuntimeSdk()); } @@ -1288,6 +1265,18 @@ public final class BuildManager implements Disposable { .orElseGet(() -> fallbackSdkProvider.apply(candidates.keySet())); } + private static @Nullable Pair getForkedJavacFallbackSdk(@NotNull Project project, int oldestPossibleVersion) { + // select the oldest SDK version among SDKs that are present in the SDK Table, but not older than + return StreamEx.of(ProjectJdkTable.getInstance().getSdksOfType(JavaSdk.getInstance())) + .filter(getWslSdkFilter(project)) + .mapToEntry(sdk -> JavaVersion.tryParse(sdk.getVersionString())) + .filterValues(version -> version != null && version.isAtLeast(oldestPossibleVersion)) + .min(Map.Entry.comparingByValue()) + .map(p -> Pair.create(p.getKey(), JavaSdkVersion.fromJavaVersion(p.getValue()))) + .filter(p -> p.second != null) + .orElse(null); + } + private static @NotNull Pair getIDERuntimeSdk() { @SuppressWarnings("removal") Sdk sdk = JavaAwareProjectJdkTableImpl.getInstanceEx().getInternalJdk(); @@ -1707,9 +1696,19 @@ public final class BuildManager implements Disposable { cmdLine.addParameter("-Djps.report.registered.unexistent.output=true"); } - if (localProject && myFallbackSdkHome != null && myFallbackSdkVersion != null) { - cmdLine.addPathParameter("-D" + GlobalOptions.FALLBACK_JDK_HOME + '=', myFallbackSdkHome); - cmdLine.addParameter("-D" + GlobalOptions.FALLBACK_JDK_VERSION + '=' + myFallbackSdkVersion); + String fallbackSdkHome = localProject? System.getProperty(GlobalOptions.FALLBACK_JDK_HOME, null) : null; + String fallbackSdkVersion = localProject? System.getProperty(GlobalOptions.FALLBACK_JDK_VERSION, null) : null; + if (fallbackSdkHome == null || fallbackSdkVersion == null) { + Pair fallback = getForkedJavacFallbackSdk(project, ExternalJavacProcess.MINIMUM_REQUIRED_JAVA_VERSION); + Sdk sdk = fallback != null? fallback.first : null; + if (sdk != null && sdk.getSdkType() instanceof JavaSdkType javaSdk && !FileUtil.pathsEqual(vmExecutablePath, javaSdk.getVMExecutablePath(sdk))) { + fallbackSdkHome = sdk.getHomePath(); + fallbackSdkVersion = fallback.second.getMaxLanguageLevel().getShortText(); + } + } + if (fallbackSdkHome != null && fallbackSdkVersion != null) { + cmdLine.addPathParameter("-D" + GlobalOptions.FALLBACK_JDK_HOME + '=', fallbackSdkHome); + cmdLine.addParameter("-D" + GlobalOptions.FALLBACK_JDK_VERSION + '=' + fallbackSdkVersion); } cmdLine.addParameter("-Dio.netty.noUnsafe=true"); diff --git a/jps/jps-builders/resources/messages/JpsBuildBundle.properties b/jps/jps-builders/resources/messages/JpsBuildBundle.properties index 953c8d92b618..2f38bad4a6d9 100644 --- a/jps/jps-builders/resources/messages/JpsBuildBundle.properties +++ b/jps/jps-builders/resources/messages/JpsBuildBundle.properties @@ -68,6 +68,15 @@ build.message.unsupported.javac.version=Cannot start javac process for {0}: it i but IDE supports compilation using JDK {2} and newer only.\nConsider associating the module with a supported JDK version. Note, that \ it is possible to keep the current Java language level to make produced *.class files compatible with Java {3} even when they are compiled by a newer compiler version. +build.message.fallback.javac.used=Module ''{0}'' is configured for JVM target {1}. Compiling the module using java compiler {2} from the configured fallback SDK. +build.message.cannot.compile.fallback.jdk.not.supported=Cannot compile module ''{0}'' configured for JVM target {1}: the specified fallback SDK version {2} is not supported by the IDE. +build.message.cannot.compile.fallback.jdk.unsupported.jvm.target=Cannot compile module ''{0}'' configured for JVM target {1}: the specified fallback SDK version {2} does not support the required jvm target {1}. +build.message.cannot.compile.associated.jdk.not.supported=Cannot compile module ''{0}'' configured for JVM target {1}: the JDK {2}, currently associated with the module, is not supported by the IDE. +build.message.cannot.compile.associated.jdk.unsupported.jvm.target=Cannot compile module ''{0}'' configured for JVM target {1}: the JDK {2}, currently associated with the module, does not support the required jvm target {1}. +build.message.fallback.jdk.hint=Please add JDK {0} or later to Project Structure | Platform Settings | SDKs. \ + The added JDK should support cross-compilation for JVM target {1}. \ + This JDK will be automatically picked by the IDE to compile for older JVM targets. + #artifacts builder.name.artifacts.builder=Artifacts builder build.message.archive.0.doesn.t.contain.files.so.it.won.t.be.created=Archive ''{0}'' doesn''t contain files so it won''t be created diff --git a/jps/jps-builders/src/org/jetbrains/jps/api/GlobalOptions.java b/jps/jps-builders/src/org/jetbrains/jps/api/GlobalOptions.java index 0cc34cb810c1..61eb0fcfc0f5 100644 --- a/jps/jps-builders/src/org/jetbrains/jps/api/GlobalOptions.java +++ b/jps/jps-builders/src/org/jetbrains/jps/api/GlobalOptions.java @@ -37,6 +37,10 @@ public interface GlobalOptions { // notification about the files changed during compilation, but not compiled in current compilation session String JPS_UNPROCESSED_FS_CHANGES_MESSAGE_ID = "!unprocessed_fs_changes_detected!"; + // notification about necessary fallback SDK setup to enable compilation for older JVM targets + @ApiStatus.Internal + String JPS_FALLBACK_SDK_SETUP_MESSAGE_ID = "!fallback_sdk_setup_required!"; + /** * The path to external project config directory (used for external system projects). */ diff --git a/jps/jps-builders/src/org/jetbrains/jps/incremental/java/JavaBuilder.java b/jps/jps-builders/src/org/jetbrains/jps/incremental/java/JavaBuilder.java index 27ba4e848004..2b394ae4810a 100644 --- a/jps/jps-builders/src/org/jetbrains/jps/incremental/java/JavaBuilder.java +++ b/jps/jps-builders/src/org/jetbrains/jps/incremental/java/JavaBuilder.java @@ -39,6 +39,7 @@ import org.jetbrains.jps.cmdline.ProjectDescriptor; import org.jetbrains.jps.incremental.*; import org.jetbrains.jps.incremental.messages.BuildMessage; import org.jetbrains.jps.incremental.messages.CompilerMessage; +import org.jetbrains.jps.incremental.messages.FallbackJdkSetupNotification; import org.jetbrains.jps.incremental.messages.ProgressMessage; import org.jetbrains.jps.incremental.storage.BuildDataManager; import org.jetbrains.jps.javac.*; @@ -446,7 +447,7 @@ public final class JavaBuilder extends ModuleLevelBuilder { // when forking external javac, compilers from SDK 1.7 and higher are supported final Pair forkSdk; if (shouldForkCompilerProcess(context, chunk, targetLanguageLevel, compilingTool)) { - forkSdk = getForkedJavacSdk(diagnosticSink, chunk, targetLanguageLevel); + forkSdk = getForkedJavacSdk(context, diagnosticSink, chunk, targetLanguageLevel); if (forkSdk == null) { return false; } @@ -1203,7 +1204,7 @@ public final class JavaBuilder extends ModuleLevelBuilder { return chunkSdkVersion; } - private static @Nullable Pair getForkedJavacSdk(DiagnosticListener diagnostic, ModuleChunk chunk, int targetLanguageLevel) { + private static @Nullable Pair getForkedJavacSdk(CompileContext context, DiagnosticListener diagnostic, ModuleChunk chunk, int targetLanguageLevel) { final Pair, Integer> associatedSdk = getAssociatedSdk(chunk); boolean canRunAssociatedJavac = false; if (associatedSdk != null) { @@ -1238,33 +1239,52 @@ public final class JavaBuilder extends ModuleLevelBuilder { final int fallbackVersion = JpsJavaSdkType.parseVersion(fallbackJdkVersion); if (isTargetReleaseSupported(fallbackVersion, targetLanguageLevel)) { if (fallbackVersion >= ExternalJavacProcess.MINIMUM_REQUIRED_JAVA_VERSION) { + diagnostic.report(new PlainMessageDiagnostic(Diagnostic.Kind.NOTE, + JpsBuildBundle.message( + "build.message.fallback.javac.used", + chunk.getName(), + targetLanguageLevel, + fallbackJdkVersion + ) + )); return Pair.create(fallbackJdkHome, fallbackVersion); } else { + // the configured fallback SDK supports the required JVM target, but cannot be run by JPS LOG.info("Version string for fallback JDK is '" + fallbackJdkVersion + "' (recognized as version '" + fallbackVersion + "')." + " At least version " + ExternalJavacProcess.MINIMUM_REQUIRED_JAVA_VERSION + " is required to launch javac process."); + + diagnostic.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, + JpsBuildBundle.message("build.message.cannot.compile.fallback.jdk.not.supported", chunk.getName(), targetLanguageLevel, fallbackJdkVersion) + )); } } - } - - // at this point, fallbackJdk is not suitable too - if (associatedSdk != null) { - if (canRunAssociatedJavac) { - // although target release is not supported, attempt to start javac, so that javac properly reports this error - return Pair.create(associatedSdk.first.getHomePath(), associatedSdk.second); - } else { + // configured fallback SDK does not support the required jvm target diagnostic.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, - JpsBuildBundle.message( - "build.message.unsupported.javac.version", - chunk.getName(), - associatedSdk.second, - ExternalJavacProcess.MINIMUM_REQUIRED_JAVA_VERSION, - targetLanguageLevel - ) + JpsBuildBundle.message("build.message.cannot.compile.fallback.jdk.unsupported.jvm.target", chunk.getName(), targetLanguageLevel, fallbackJdkVersion) )); } } + else { + // fallback JDK is not configured at all; + // associatedSdk != null, is configured, but either JPS cannot run it, or it does not support the required jvm target + + String errorMessage; + if (canRunAssociatedJavac) { + errorMessage = JpsBuildBundle.message("build.message.cannot.compile.associated.jdk.unsupported.jvm.target", chunk.getName(), targetLanguageLevel, associatedSdk.first.getVersionString()); + } + else { + errorMessage = JpsBuildBundle.message("build.message.cannot.compile.associated.jdk.not.supported", chunk.getName(), targetLanguageLevel, associatedSdk.first.getVersionString()); + } + diagnostic.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, errorMessage)); + } + + String fallbackSDKHint = JpsBuildBundle.message( + "build.message.fallback.jdk.hint", ExternalJavacProcess.MINIMUM_REQUIRED_JAVA_VERSION, targetLanguageLevel + ); + context.processMessage(new FallbackJdkSetupNotification(fallbackSDKHint)); + diagnostic.report(new PlainMessageDiagnostic(Diagnostic.Kind.ERROR, fallbackSDKHint)); return null; } diff --git a/jps/jps-builders/src/org/jetbrains/jps/incremental/messages/FallbackJdkSetupNotification.java b/jps/jps-builders/src/org/jetbrains/jps/incremental/messages/FallbackJdkSetupNotification.java new file mode 100644 index 000000000000..b92583037734 --- /dev/null +++ b/jps/jps-builders/src/org/jetbrains/jps/incremental/messages/FallbackJdkSetupNotification.java @@ -0,0 +1,17 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.jps.incremental.messages; + +import com.intellij.openapi.util.NlsSafe; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.jps.api.GlobalOptions; + +@ApiStatus.Internal +public final class FallbackJdkSetupNotification extends CustomBuilderMessage{ + public FallbackJdkSetupNotification(@NlsSafe String notificationMessage) { + super( + GlobalOptions.JPS_SYSTEM_BUILDER_ID, + GlobalOptions.JPS_FALLBACK_SDK_SETUP_MESSAGE_ID, + notificationMessage + ); + } +}