Skip to content

Commit 193c879

Browse files
authored
Support launching Multi-Release-Compiled Projects (#753)
Currently when one launches a multi-release compiled project one always gets the type from the default project folder. This now first checks what JRE is used to launch, then adds all valid folders in reverse version order to the classpath to emulate the behavior of loading a multi-release jar at runtime. Fix https://github.com/eclipse-jdt/eclipse.jdt.core/issues/4276
1 parent fc76020 commit 193c879

File tree

18 files changed

+448
-85
lines changed

18 files changed

+448
-85
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package p;
2+
3+
public class Main {
4+
5+
public static void main(String[] args) {
6+
System.out.println("Java: "+java.lang.Runtime.version().feature());
7+
new X().greet();
8+
}
9+
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package p;
2+
3+
public class X {
4+
public void greet() {
5+
System.out.println("X: 11");
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package p;
2+
3+
public class Y {
4+
public void y() {
5+
System.out.println("Y: 11");
6+
}
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package p;
2+
3+
public class X {
4+
public void greet() {
5+
System.out.println("X: 17");
6+
new Y().y();
7+
}
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package p;
2+
3+
public class Z {
4+
public static void z() {
5+
System.out.println("Z: 17");
6+
}
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package p;
2+
3+
public class Y {
4+
public void y() {
5+
System.out.println("Y: 21");
6+
Z.z();
7+
}
8+
}

org.eclipse.jdt.debug.tests/test plugin/org/eclipse/jdt/debug/testplugin/JavaProjectHelper.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.eclipse.core.runtime.IProgressMonitor;
3939
import org.eclipse.core.runtime.IStatus;
4040
import org.eclipse.core.runtime.Path;
41+
import org.eclipse.jdt.core.IClasspathAttribute;
4142
import org.eclipse.jdt.core.IClasspathEntry;
4243
import org.eclipse.jdt.core.IJavaProject;
4344
import org.eclipse.jdt.core.IPackageFragmentRoot;
@@ -111,6 +112,11 @@ public class JavaProjectHelper {
111112
*/
112113
public static final IPath TEST_25_SRC_DIR = new Path("java25");
113114

115+
/**
116+
* path to the multirelease test source
117+
*/
118+
public static final IPath TEST_MR_SRC_DIR = new Path("multirelease");
119+
114120
/**
115121
* path to the compiler error java file
116122
*/
@@ -323,9 +329,12 @@ public static void delete(IJavaProject jproject) throws CoreException {
323329

324330
/**
325331
* Adds a new source container specified by the container name to the source path of the specified project
332+
*
333+
* @param extra
334+
* optional extra classpath attributes
326335
* @return the package fragment root of the container name
327336
*/
328-
public static IPackageFragmentRoot addSourceContainer(IJavaProject jproject, String containerName) throws CoreException {
337+
public static IPackageFragmentRoot addSourceContainer(IJavaProject jproject, String containerName, IClasspathAttribute... extra) throws CoreException {
329338
IProject project= jproject.getProject();
330339
IContainer container= null;
331340
if (containerName == null || containerName.length() == 0) {
@@ -339,7 +348,7 @@ public static IPackageFragmentRoot addSourceContainer(IJavaProject jproject, Str
339348
}
340349
IPackageFragmentRoot root= jproject.getPackageFragmentRoot(container);
341350

342-
IClasspathEntry cpe= JavaCore.newSourceEntry(root.getPath());
351+
IClasspathEntry cpe = JavaCore.newSourceEntry(root.getPath(), ClasspathEntry.INCLUDE_ALL, ClasspathEntry.EXCLUDE_NONE, null, extra);
343352
addToClasspath(jproject, cpe);
344353
return root;
345354
}

org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ public abstract class AbstractDebugTest extends TestCase implements IEvaluation
184184
public static final String TWENTYFOUR_PROJECT_NAME = "Two_Four";
185185
public static final String TWENTYFIVE_PROJECT_NAME = "Two_Five";
186186
public static final String BOUND_JRE_PROJECT_NAME = "BoundJRE";
187+
public static final String MR_PROJECT_NAME = "MR";
187188
public static final String CLONE_SUFFIX = "Clone";
188189

189190
final String[] LAUNCH_CONFIG_NAMES_1_4 = { "LargeSourceFile", "LotsOfFields",
@@ -248,6 +249,7 @@ public abstract class AbstractDebugTest extends TestCase implements IEvaluation
248249
private static boolean loadedEE = false;
249250
private static boolean loadedJRE = false;
250251
private static boolean loadedMulti = false;
252+
private static boolean loadedMR;
251253
private static boolean welcomeClosed = false;
252254

253255
/**
@@ -285,6 +287,8 @@ protected void setUp() throws Exception {
285287
loadedEE = pro.exists();
286288
pro = ResourcesPlugin.getWorkspace().getRoot().getProject(MULTI_OUTPUT_PROJECT_NAME);
287289
loadedMulti = pro.exists();
290+
pro = ResourcesPlugin.getWorkspace().getRoot().getProject(MR_PROJECT_NAME);
291+
loadedMR = pro.exists();
288292
assertWelcomeScreenClosed();
289293
}
290294

@@ -626,6 +630,40 @@ synchronized void assert21Project() {
626630
}
627631
}
628632

633+
synchronized void assertMRProject() {
634+
IJavaProject jp = null;
635+
ArrayList<ILaunchConfiguration> cfgs = new ArrayList<>(1);
636+
try {
637+
if (!loadedMR) {
638+
jp = createProject(MR_PROJECT_NAME, JavaProjectHelper.TEST_MR_SRC_DIR.toString(), JavaProjectHelper.JAVA_SE_21_EE_NAME, false);
639+
jp.setOption(JavaCore.COMPILER_RELEASE, JavaCore.ENABLED);
640+
jp.setOption(JavaCore.COMPILER_COMPLIANCE, "11");
641+
jp.setOption(JavaCore.COMPILER_SOURCE, "11");
642+
jp.setOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, "11");
643+
IPackageFragmentRoot src17 = JavaProjectHelper.addSourceContainer(jp, "src17", JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "17"));
644+
IPackageFragmentRoot src21 = JavaProjectHelper.addSourceContainer(jp, "src21", JavaCore.newClasspathAttribute(IClasspathAttribute.RELEASE, "21"));
645+
File root = JavaTestPlugin.getDefault().getFileInPlugin(JavaProjectHelper.TEST_MR_SRC_DIR);
646+
JavaProjectHelper.importFilesFromDirectory(new File(root, src17.getPath().lastSegment()), src17.getPath(), null);
647+
JavaProjectHelper.importFilesFromDirectory(new File(root, src21.getPath().lastSegment()), src21.getPath(), null);
648+
cfgs.add(createLaunchConfiguration(jp, "p.Main"));
649+
loadedMR = true;
650+
waitForBuild();
651+
}
652+
} catch (Exception e) {
653+
try {
654+
if (jp != null) {
655+
jp.getProject().delete(true, true, null);
656+
for (int i = 0; i < cfgs.size(); i++) {
657+
cfgs.get(i).delete();
658+
}
659+
}
660+
} catch (CoreException ce) {
661+
// ignore
662+
}
663+
handleProjectCreationException(e, MR_PROJECT_NAME, jp);
664+
}
665+
}
666+
629667
synchronized void assert23Project() {
630668
IJavaProject jp = null;
631669
ArrayList<ILaunchConfiguration> cfgs = new ArrayList<>(1);
@@ -1017,6 +1055,16 @@ protected IJavaProject get21Project() {
10171055
return getJavaProject(TWENTYONE_PROJECT_NAME);
10181056
}
10191057

1058+
/**
1059+
* Returns the 'multirelease' project, used for Multirelease tests.
1060+
*
1061+
* @return the test project
1062+
*/
1063+
protected IJavaProject getMultireleaseProject() {
1064+
assertMRProject();
1065+
return getJavaProject(MR_PROJECT_NAME);
1066+
}
1067+
10201068
/**
10211069
* Returns the 'Two_Three' project, used for Java 23 tests.
10221070
*
@@ -1117,7 +1165,12 @@ protected IJavaProject createProject(String name, String contentpath, String ee,
11171165
IJavaProject jp = JavaProjectHelper.createJavaProject(name, JavaProjectHelper.BIN_DIR);
11181166
IPackageFragmentRoot src = JavaProjectHelper.addSourceContainer(jp, JavaProjectHelper.SRC_DIR);
11191167
File root = JavaTestPlugin.getDefault().getFileInPlugin(new Path(contentpath));
1120-
JavaProjectHelper.importFilesFromDirectory(root, src.getPath(), null);
1168+
File srcInRoot = new File(root, src.getPath().lastSegment());
1169+
if (srcInRoot.isDirectory()) {
1170+
JavaProjectHelper.importFilesFromDirectory(srcInRoot, src.getPath(), null);
1171+
} else {
1172+
JavaProjectHelper.importFilesFromDirectory(root, src.getPath(), null);
1173+
}
11211174

11221175
// add the EE library
11231176
IVMInstall vm = JavaRuntime.getDefaultVMInstall();

org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
import org.eclipse.jdt.debug.tests.core.LiteralTests17;
8888
import org.eclipse.jdt.debug.tests.core.LocalVariableTests;
8989
import org.eclipse.jdt.debug.tests.core.ModuleOptionsTests;
90+
import org.eclipse.jdt.debug.tests.core.MultiReleaseLaunchTests;
9091
import org.eclipse.jdt.debug.tests.core.ProcessTests;
9192
import org.eclipse.jdt.debug.tests.core.ResolveRuntimeClasspathTests;
9293
import org.eclipse.jdt.debug.tests.core.RuntimeClasspathEntryTests;
@@ -291,6 +292,7 @@ public AutomatedSuite() {
291292
addTest(new TestSuite(WorkingDirectoryTests.class));
292293
addTest(new TestSuite(EventDispatcherTest.class));
293294
addTest(new TestSuite(SyntheticVariableTests.class));
295+
addTest(new TestSuite(MultiReleaseLaunchTests.class));
294296

295297
// Refactoring tests
296298
//TODO: project rename
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Christoph Läubrich and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Christoph Läubrich - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.jdt.debug.tests.core;
15+
16+
import java.io.File;
17+
import java.io.StringReader;
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.HashSet;
21+
import java.util.Iterator;
22+
import java.util.List;
23+
import java.util.Properties;
24+
import java.util.Set;
25+
import java.util.UUID;
26+
27+
import org.eclipse.core.runtime.NullProgressMonitor;
28+
import org.eclipse.debug.core.DebugPlugin;
29+
import org.eclipse.debug.core.ILaunchConfiguration;
30+
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
31+
import org.eclipse.debug.ui.DebugUITools;
32+
import org.eclipse.jdt.core.IJavaProject;
33+
import org.eclipse.jdt.debug.core.IJavaDebugTarget;
34+
import org.eclipse.jdt.debug.tests.ui.AbstractDebugUiTests;
35+
import org.eclipse.jdt.internal.launching.DetectVMInstallationsJob;
36+
import org.eclipse.jdt.launching.IVMInstall;
37+
import org.eclipse.jdt.launching.IVMInstall2;
38+
import org.eclipse.jdt.launching.IVMInstallType;
39+
import org.eclipse.jdt.launching.JavaRuntime;
40+
import org.eclipse.jdt.launching.VMStandin;
41+
import org.eclipse.jface.text.IDocument;
42+
import org.eclipse.ui.console.IConsole;
43+
import org.eclipse.ui.console.TextConsole;
44+
45+
/**
46+
* <b>IMPORTANT</b> This test requires some different JVM installs to be present (see {@link #JAVA_11}, {@link #JAVA_17}, {@link #JAVA_21})) if such
47+
* JVMs can not be found, the test will fail! One can specify a basedir to search for such jvms with the {@link #JVM_SEARCH_BASE} system property.
48+
*/
49+
public class MultiReleaseLaunchTests extends AbstractDebugUiTests {
50+
51+
private static final String JVM_SEARCH_BASE = "MultiReleaseLaunchTests.rootDir";
52+
private static final RequiredJavaVersion JAVA_11 = new RequiredJavaVersion(11, 16);
53+
private static final RequiredJavaVersion JAVA_17 = new RequiredJavaVersion(17, 20);
54+
private static final RequiredJavaVersion JAVA_21 = new RequiredJavaVersion(21, Integer.MAX_VALUE);
55+
56+
private List<Runnable> disposeVms = new ArrayList<>();
57+
58+
public MultiReleaseLaunchTests(String name) {
59+
super(name);
60+
}
61+
62+
@Override
63+
protected void setUp() throws Exception {
64+
super.setUp();
65+
final Set<File> existingLocations = new HashSet<>();
66+
List<RequiredJavaVersion> requiredJavaVersions = new ArrayList<>(List.of(JAVA_11, JAVA_17, JAVA_21));
67+
removeExistingJavaVersions(requiredJavaVersions, existingLocations);
68+
if (!requiredJavaVersions.isEmpty()) {
69+
final File rootDir = new File(System.getProperty(JVM_SEARCH_BASE, "/opt/tools/java/openjdk/"));
70+
final List<File> locations = new ArrayList<>();
71+
final List<IVMInstallType> types = new ArrayList<>();
72+
DetectVMInstallationsJob.search(rootDir, locations, types, existingLocations, new NullProgressMonitor());
73+
for (int i = 0; i < locations.size(); i++) {
74+
File location = locations.get(i);
75+
IVMInstallType type = types.get(i);
76+
String id = "MultiReleaseLaunchTests-" + UUID.randomUUID() + "-" + i;
77+
VMStandin workingCopy = new VMStandin(type, id);
78+
workingCopy.setInstallLocation(location);
79+
workingCopy.setName(id);
80+
IVMInstall install = workingCopy.convertToRealVM();
81+
if (removeIfMatch(requiredJavaVersions, install)) {
82+
disposeVms.add(() -> type.disposeVMInstall(id));
83+
} else {
84+
type.disposeVMInstall(id);
85+
}
86+
}
87+
}
88+
assertTrue("The following java versions are required by this test but can not be found: "
89+
+ requiredJavaVersions, requiredJavaVersions.isEmpty());
90+
}
91+
92+
@Override
93+
protected void tearDown() throws Exception {
94+
super.tearDown();
95+
disposeVms.forEach(Runnable::run);
96+
}
97+
98+
@Override
99+
protected IJavaProject getProjectContext() {
100+
return getMultireleaseProject();
101+
}
102+
103+
public void testMultiReleaseLaunch() throws Exception {
104+
ILaunchConfiguration config = getLaunchConfiguration("p.Main");
105+
Properties result = launchAndReadResult(config, 11);
106+
assertTrue("Was not launched with a proper Java installation " + result, JAVA_11.matches(result.getProperty("Java")));
107+
assertEquals("X should be executed from Java 11 version: " + result, "11", result.get("X"));
108+
assertNull("Y should not be executed from Java 11 version: " + result, result.get("Y"));
109+
assertNull("Z should not be executed from Java 11 version: " + result, result.get("Z"));
110+
Properties result17 = launchAndReadResult(config, 17);
111+
assertTrue("Was not launched with a proper Java installation " + result17, JAVA_17.matches(result17.getProperty("Java")));
112+
assertEquals("X should be executed from Java 17 version: " + result17, "17", result17.get("X"));
113+
assertEquals("Y should be executed from Java 11 version: " + result17, "11", result17.get("Y"));
114+
assertNull("Z should not be executed from Java 17 version: " + result17, result17.get("Z"));
115+
Properties result21 = launchAndReadResult(config, 21);
116+
assertTrue("Was not launched with a proper Java installation " + result21, JAVA_21.matches(result21.getProperty("Java")));
117+
assertEquals("X should be executed from Java 17 version: " + result21, "17", result21.get("X"));
118+
assertEquals("Y should be executed from Java 21 version: " + result21, "21", result21.get("Y"));
119+
assertEquals("Z should be executed from Java 17 version: " + result21, "17", result21.get("Z"));
120+
}
121+
122+
private Properties launchAndReadResult(ILaunchConfiguration config, int javaVersion) throws Exception {
123+
ILaunchConfigurationWorkingCopy workingCopy = config.getWorkingCopy();
124+
workingCopy.setAttribute("org.eclipse.jdt.launching.JRE_CONTAINER", "org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-"
125+
+ javaVersion + "/");
126+
Properties properties = new Properties();
127+
IJavaDebugTarget target = launchAndTerminate(workingCopy.doSave(), DEFAULT_TIMEOUT);
128+
processUiEvents();
129+
final IConsole console = DebugUITools.getConsole(target.getProcess());
130+
final TextConsole textConsole = (TextConsole) console;
131+
final IDocument consoleDocument = textConsole.getDocument();
132+
String content = consoleDocument.get();
133+
properties.load(new StringReader(content));
134+
DebugPlugin.getDefault().getLaunchManager().removeLaunch(target.getLaunch());
135+
return properties;
136+
}
137+
138+
private static int getJavaVersion(IVMInstall install) {
139+
if (install instanceof IVMInstall2 vm) {
140+
try {
141+
String javaVersion = vm.getJavaVersion().split("\\.")[0]; //$NON-NLS-1$
142+
return Integer.parseInt(javaVersion);
143+
} catch (RuntimeException rte) {
144+
// can't know then...
145+
}
146+
}
147+
return -1;
148+
}
149+
150+
private static void removeExistingJavaVersions(Collection<RequiredJavaVersion> requiredJavaVersions, Set<File> existingLocations) {
151+
IVMInstallType[] installTypes = JavaRuntime.getVMInstallTypes();
152+
for (IVMInstallType installType : installTypes) {
153+
IVMInstall[] vmInstalls = installType.getVMInstalls();
154+
for (IVMInstall install : vmInstalls) {
155+
if (requiredJavaVersions.isEmpty()) {
156+
return;
157+
}
158+
existingLocations.add(install.getInstallLocation());
159+
removeIfMatch(requiredJavaVersions, install);
160+
}
161+
}
162+
}
163+
164+
protected static boolean removeIfMatch(Collection<RequiredJavaVersion> requiredJavaVersions, IVMInstall install) {
165+
int javaVersion = getJavaVersion(install);
166+
for (Iterator<RequiredJavaVersion> iterator = requiredJavaVersions.iterator(); iterator.hasNext();) {
167+
if (iterator.next().matches(javaVersion)) {
168+
iterator.remove();
169+
return true;
170+
}
171+
}
172+
return false;
173+
}
174+
175+
private static record RequiredJavaVersion(int from, int to) {
176+
177+
public boolean matches(int version) {
178+
return (version >= from() && version <= to());
179+
}
180+
181+
public boolean matches(String v) {
182+
return matches(Integer.parseInt(v));
183+
}
184+
}
185+
186+
}

0 commit comments

Comments
 (0)