Skip to content

Commit 1864c7f

Browse files
committed
feat: support Java 25 main methods
1 parent d5b3e64 commit 1864c7f

9 files changed

Lines changed: 166 additions & 11 deletions

File tree

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ project.ext.dependencyStrings = [
3535
JACKSON_DATABIND: 'com.fasterxml.jackson.core:jackson-databind',
3636
JACKSON_DATAFORMAT_YAML: 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml',
3737
JACKSON_DATATYPE_JSR310: 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310',
38-
ASM: 'org.ow2.asm:asm:9.8',
38+
ASM: 'org.ow2.asm:asm:9.9',
3939
PICOCLI: 'info.picocli:picocli:4.7.4',
4040

4141
MAVEN_API: 'org.apache.maven:maven-plugin-api:3.9.3',

jib-core/src/main/java/com/google/cloud/tools/jib/api/MainClassFinder.java

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,20 @@
3232
import org.objectweb.asm.Opcodes;
3333

3434
/**
35-
* Finds main classes in a list of class files. Main classes are classes that define the {@code
36-
* public static void main(String[] args)} method.
35+
* Finds main classes in a list of class files. Main classes are classes that define a valid main
36+
* method.
37+
*
38+
* <p>For class files compiled with Java 25 or later (JEP 512), valid main methods include:
39+
*
40+
* <ul>
41+
* <li>{@code static void main(String[] args)} - with public, protected, or package-private access
42+
* <li>{@code static void main()} - static main without parameters
43+
* <li>{@code void main(String[] args)} - instance main with parameters
44+
* <li>{@code void main()} - instance main without parameters
45+
* </ul>
46+
*
47+
* <p>For class files compiled with earlier Java versions, only the traditional {@code public static
48+
* void main(String[] args)} is recognized.
3749
*/
3850
public class MainClassFinder {
3951

@@ -106,36 +118,85 @@ public List<String> getFoundMainClasses() {
106118
/** {@link ClassVisitor} that keeps track of whether or not it has visited a main class. */
107119
private static class MainClassVisitor extends ClassVisitor {
108120

109-
/** The return/argument types for main. */
110-
private static final String MAIN_DESCRIPTOR =
121+
/** Java 25 class file major version (flexible main methods finalized). */
122+
private static final int JAVA_25_CLASS_VERSION = 69;
123+
124+
/** The return/argument types for main with String[] parameter. */
125+
private static final String MAIN_WITH_ARGS_DESCRIPTOR =
111126
org.objectweb.asm.Type.getMethodDescriptor(
112127
org.objectweb.asm.Type.VOID_TYPE, org.objectweb.asm.Type.getType(String[].class));
113128

114-
/** Accessors that main may or may not have. */
115-
private static final int OPTIONAL_ACCESS =
129+
/** The return/argument types for main without parameters. */
130+
private static final String MAIN_NO_ARGS_DESCRIPTOR =
131+
org.objectweb.asm.Type.getMethodDescriptor(org.objectweb.asm.Type.VOID_TYPE);
132+
133+
/** Optional modifiers that main may or may not have. */
134+
private static final int OPTIONAL_MODIFIERS =
116135
Opcodes.ACC_FINAL | Opcodes.ACC_DEPRECATED | Opcodes.ACC_VARARGS | Opcodes.ACC_SYNTHETIC;
117136

118137
private boolean visitedMainClass;
138+
private int classVersion;
119139

120140
private MainClassVisitor() {
121141
super(Opcodes.ASM9);
122142
}
123143

144+
@Override
145+
public void visit(
146+
int version,
147+
int access,
148+
String name,
149+
String signature,
150+
String superName,
151+
String[] interfaces) {
152+
this.classVersion = version;
153+
super.visit(version, access, name, signature, superName, interfaces);
154+
}
155+
124156
@Override
125157
@Nullable
126158
public MethodVisitor visitMethod(
127159
int access, String name, String descriptor, String signature, String[] exceptions) {
128-
if ((access & ~OPTIONAL_ACCESS) == (Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC)
129-
&& name.equals("main")
130-
&& descriptor.equals(MAIN_DESCRIPTOR)) {
160+
if (!name.equals("main")) {
161+
return null;
162+
}
163+
164+
if ((access & Opcodes.ACC_PRIVATE) != 0) {
165+
return null;
166+
}
167+
168+
// For class files before Java 25, only traditional main is valid
169+
if (classVersion < JAVA_25_CLASS_VERSION) {
170+
// Traditional main: public static void main(String[] args)
171+
int requiredAccess = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC;
172+
if ((access & ~OPTIONAL_MODIFIERS) == requiredAccess
173+
&& descriptor.equals(MAIN_WITH_ARGS_DESCRIPTOR)) {
174+
visitedMainClass = true;
175+
}
176+
return null;
177+
}
178+
179+
// For Java 25+, check flexible main method signatures (JEP 512)
180+
boolean isValidDescriptor =
181+
descriptor.equals(MAIN_WITH_ARGS_DESCRIPTOR)
182+
|| descriptor.equals(MAIN_NO_ARGS_DESCRIPTOR);
183+
184+
if (!isValidDescriptor) {
185+
return null;
186+
}
187+
188+
int relevantAccess =
189+
access & ~(Opcodes.ACC_PUBLIC | Opcodes.ACC_PROTECTED | OPTIONAL_MODIFIERS);
190+
if (relevantAccess == Opcodes.ACC_STATIC || relevantAccess == 0) {
131191
visitedMainClass = true;
132192
}
193+
133194
return null;
134195
}
135196
}
136197

137198
/**
138-
* Tries to find classes with {@code psvm} (see class javadoc) in {@code files}.
199+
* Tries to find classes with valid main methods (see class javadoc) in {@code files}.
139200
*
140201
* @param files the files to search
141202
* @param logger a {@link Consumer} used to handle log events

jib-core/src/test/java/com/google/cloud/tools/jib/api/MainClassFinderTest.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,98 @@ public void testMainClass_synthetic() throws URISyntaxException, IOException {
149149
MatcherAssert.assertThat(
150150
mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("HelloWorldKt"));
151151
}
152+
153+
@Test
154+
public void testMainClass_java25StaticNoArgs() throws URISyntaxException, IOException {
155+
Path rootDirectory =
156+
Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI());
157+
Path classFile = rootDirectory.resolve("StaticMainNoArgs.class");
158+
MainClassFinder.Result mainClassFinderResult =
159+
MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer);
160+
Assert.assertSame(
161+
MainClassFinder.Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType());
162+
MatcherAssert.assertThat(
163+
mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("StaticMainNoArgs"));
164+
}
165+
166+
@Test
167+
public void testMainClass_java25InstanceWithArgs() throws URISyntaxException, IOException {
168+
Path rootDirectory =
169+
Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI());
170+
Path classFile = rootDirectory.resolve("InstanceMainWithArgs.class");
171+
MainClassFinder.Result mainClassFinderResult =
172+
MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer);
173+
Assert.assertSame(
174+
MainClassFinder.Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType());
175+
MatcherAssert.assertThat(
176+
mainClassFinderResult.getFoundMainClass(),
177+
CoreMatchers.containsString("InstanceMainWithArgs"));
178+
}
179+
180+
@Test
181+
public void testMainClass_java25InstanceNoArgs() throws URISyntaxException, IOException {
182+
Path rootDirectory =
183+
Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI());
184+
Path classFile = rootDirectory.resolve("InstanceMainNoArgs.class");
185+
MainClassFinder.Result mainClassFinderResult =
186+
MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer);
187+
Assert.assertSame(
188+
MainClassFinder.Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType());
189+
MatcherAssert.assertThat(
190+
mainClassFinderResult.getFoundMainClass(),
191+
CoreMatchers.containsString("InstanceMainNoArgs"));
192+
}
193+
194+
@Test
195+
public void testMainClass_java25ProtectedMain() throws URISyntaxException, IOException {
196+
Path rootDirectory =
197+
Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI());
198+
Path classFile = rootDirectory.resolve("ProtectedMain.class");
199+
MainClassFinder.Result mainClassFinderResult =
200+
MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer);
201+
Assert.assertSame(
202+
MainClassFinder.Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType());
203+
MatcherAssert.assertThat(
204+
mainClassFinderResult.getFoundMainClass(), CoreMatchers.containsString("ProtectedMain"));
205+
}
206+
207+
@Test
208+
public void testMainClass_java25PackagePrivateMain() throws URISyntaxException, IOException {
209+
Path rootDirectory =
210+
Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI());
211+
Path classFile = rootDirectory.resolve("PackagePrivateMain.class");
212+
MainClassFinder.Result mainClassFinderResult =
213+
MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer);
214+
Assert.assertSame(
215+
MainClassFinder.Result.Type.MAIN_CLASS_FOUND, mainClassFinderResult.getType());
216+
MatcherAssert.assertThat(
217+
mainClassFinderResult.getFoundMainClass(),
218+
CoreMatchers.containsString("PackagePrivateMain"));
219+
}
220+
221+
@Test
222+
public void testMainClass_java25MultipleFlexibleMains() throws URISyntaxException, IOException {
223+
Path rootDirectory =
224+
Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI());
225+
MainClassFinder.Result mainClassFinderResult =
226+
MainClassFinder.find(new DirectoryWalker(rootDirectory).walk(), logEventConsumer);
227+
Assert.assertEquals(Result.Type.MULTIPLE_MAIN_CLASSES, mainClassFinderResult.getType());
228+
Assert.assertEquals(5, mainClassFinderResult.getFoundMainClasses().size());
229+
Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("StaticMainNoArgs"));
230+
Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("InstanceMainWithArgs"));
231+
Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("InstanceMainNoArgs"));
232+
Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("ProtectedMain"));
233+
Assert.assertTrue(mainClassFinderResult.getFoundMainClasses().contains("PackagePrivateMain"));
234+
}
235+
236+
@Test
237+
public void testMainClass_java25PrivateMainNotAllowed() throws URISyntaxException, IOException {
238+
Path rootDirectory =
239+
Paths.get(Resources.getResource("core/class-finder-tests/java25-flexible-main").toURI());
240+
Path classFile = rootDirectory.resolve("PrivateMain.class");
241+
MainClassFinder.Result mainClassFinderResult =
242+
MainClassFinder.find(java.util.Collections.singletonList(classFile), logEventConsumer);
243+
Assert.assertSame(
244+
MainClassFinder.Result.Type.MAIN_CLASS_NOT_FOUND, mainClassFinderResult.getType());
245+
}
152246
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)