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
This commit is contained in:
Eugene Zhuravlev
2025-08-11 19:25:25 +00:00
committed by intellij-monorepo-bot
parent 77536725b9
commit 67bc554792
6 changed files with 156 additions and 74 deletions
@@ -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,
@@ -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<TaskFuture<?>, Project> myAutomakeFutures = Collections.synchronizedMap(new HashMap<>());
private final Map<String, RequestFuture<?>> myBuildsInProgress = Collections.synchronizedMap(new HashMap<>());
private final Map<String, Future<Pair<RequestFuture<PreloadedProcessMessageHandler>, 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<Project> 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<WSLDistribution> projectWslDistribution = new Supplier<>() {
private WSLDistribution val;
@Override
public WSLDistribution get() {
return val != null ? val : (val = findWSLDistribution(project));
}
};
Predicate<Sdk> 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<Sdk> 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<Sdk> getWslSdkFilter(@NotNull Project project) {
// if WSL is configured, accepts only those SDKs that match project's WSL VM
Supplier<WSLDistribution> projectWslDistribution = new Supplier<>() {
private Ref<WSLDistribution> 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<Sdk, JavaSdkVersion> getForkedJavacFallbackSdk(@NotNull Project project, int oldestPossibleVersion) {
// select the oldest SDK version among SDKs that are present in the SDK Table, but not older than <oldestPossibleVersion>
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<Sdk, JavaSdkVersion> 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<Sdk, JavaSdkVersion> 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");
@@ -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
@@ -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).
*/
@@ -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<String, Integer> 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<String, Integer> getForkedJavacSdk(DiagnosticListener<? super JavaFileObject> diagnostic, ModuleChunk chunk, int targetLanguageLevel) {
private static @Nullable Pair<String, Integer> getForkedJavacSdk(CompileContext context, DiagnosticListener<? super JavaFileObject> diagnostic, ModuleChunk chunk, int targetLanguageLevel) {
final Pair<JpsSdk<JpsDummyElement>, 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;
}
@@ -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
);
}
}