From ce40f404e33b348c3d62962fe4fe34a0c7d4160c Mon Sep 17 00:00:00 2001 From: Tad Cordle Date: Thu, 17 May 2018 14:42:21 -0400 Subject: [PATCH] Infer main class if it isn't configured anywhere (#278) --- .../jib/frontend/HelpfulSuggestions.java | 4 + .../tools/jib/frontend/MainClassFinder.java | 96 ++++++++++++++++++ .../jib/frontend/HelpfulSuggestionsTest.java | 3 + .../jib/frontend/MainClassFinderTest.java | 63 ++++++++++++ .../multiple/HelloWorld.class | Bin 0 -> 534 bytes .../class-finder-tests/multiple/NotMain.class | Bin 0 -> 448 bytes .../multiple/multi/Layer1Class.class | Bin 0 -> 337 bytes .../multiple/multi/Layer2Class.class | Bin 0 -> 270 bytes .../multiple/multi/layered/HelloMoon.class | Bin 0 -> 558 bytes .../class-finder-tests/no-main/NotMain.class | Bin 0 -> 448 bytes .../no-main/multi/Layer1Class.class | Bin 0 -> 337 bytes .../no-main/multi/Layer2Class.class | Bin 0 -> 270 bytes .../simple/HelloWorld.class | Bin 0 -> 618 bytes .../simple/NotAClassButMightBe.class | 0 .../simple/NotEvenAClass.txt | 1 + .../class-finder-tests/simple/NotMain.class | Bin 0 -> 448 bytes .../subdirectories/multi/Layer1Class.class | Bin 0 -> 337 bytes .../multi/layered/HelloWorld.class | Bin 0 -> 562 bytes .../multi/layered/Layer2Class.class | Bin 0 -> 286 bytes .../resources/projects/empty/build.gradle | 1 - .../resources/projects/simple/build.gradle | 1 - .../tools/jib/gradle/BuildDockerTask.java | 3 +- .../tools/jib/gradle/BuildImageTask.java | 3 +- .../tools/jib/gradle/DockerContextTask.java | 3 +- .../tools/jib/gradle/ProjectProperties.java | 83 +++++++++++---- .../jib/gradle/ProjectPropertiesTest.java | 16 ++- .../tools/jib/maven/BuildDockerMojo.java | 2 +- .../cloud/tools/jib/maven/BuildImageMojo.java | 2 +- .../tools/jib/maven/DockerContextMojo.java | 2 +- .../tools/jib/maven/ProjectProperties.java | 84 ++++++++++++--- .../jib/maven/ProjectPropertiesTest.java | 20 +++- .../src/test/resources/projects/empty/pom.xml | 1 - .../test/resources/projects/simple/pom.xml | 1 - 33 files changed, 340 insertions(+), 49 deletions(-) create mode 100644 jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java create mode 100644 jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java create mode 100644 jib-core/src/test/resources/class-finder-tests/multiple/HelloWorld.class create mode 100644 jib-core/src/test/resources/class-finder-tests/multiple/NotMain.class create mode 100644 jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer1Class.class create mode 100644 jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer2Class.class create mode 100644 jib-core/src/test/resources/class-finder-tests/multiple/multi/layered/HelloMoon.class create mode 100644 jib-core/src/test/resources/class-finder-tests/no-main/NotMain.class create mode 100644 jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer1Class.class create mode 100644 jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer2Class.class create mode 100644 jib-core/src/test/resources/class-finder-tests/simple/HelloWorld.class create mode 100644 jib-core/src/test/resources/class-finder-tests/simple/NotAClassButMightBe.class create mode 100644 jib-core/src/test/resources/class-finder-tests/simple/NotEvenAClass.txt create mode 100644 jib-core/src/test/resources/class-finder-tests/simple/NotMain.class create mode 100644 jib-core/src/test/resources/class-finder-tests/subdirectories/multi/Layer1Class.class create mode 100644 jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/HelloWorld.class create mode 100644 jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/Layer2Class.class diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java index adb82264f1..b80b4f0f3e 100644 --- a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestions.java @@ -100,6 +100,10 @@ public String forDockerContextInsecureRecursiveDelete(String directory) { return suggest("clear " + directory + " manually before creating the Docker context"); } + public String forMainClassNotFound(String pluginName) { + return suggest("add a `mainClass` configuration to " + pluginName); + } + public String none() { return messagePrefix; } diff --git a/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java new file mode 100644 index 0000000000..3cff4957b3 --- /dev/null +++ b/jib-core/src/main/java/com/google/cloud/tools/jib/frontend/MainClassFinder.java @@ -0,0 +1,96 @@ +/* + * Copyright 2018 Google LLC. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import com.google.cloud.tools.jib.filesystem.DirectoryWalker; +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.Nullable; + +/** Infers the main class in an application. */ +public class MainClassFinder { + + /** Helper for loading a .class file. */ + private static class ClassFileLoader extends ClassLoader { + + private final Path classFile; + + private ClassFileLoader(Path classFile) { + this.classFile = classFile; + } + + @Nullable + @Override + public Class findClass(@Nullable String name) { + try { + byte[] bytes = Files.readAllBytes(classFile); + return defineClass(name, bytes, 0, bytes.length); + } catch (IOException | ClassFormatError ignored) { + // Not a valid class file + return null; + } + } + } + + /** + * Searches for a .class file containing a main method in a root directory. + * + * @return the name of the class if one is found, null if no class is found. + * @throws IOException if searching/reading files fails. + */ + public static List findMainClasses(Path rootDirectory) throws IOException { + List classNames = new ArrayList<>(); + + // Make sure rootDirectory is valid + if (!Files.exists(rootDirectory) || !Files.isDirectory(rootDirectory)) { + return classNames; + } + + // Get all .class files + new DirectoryWalker(rootDirectory) + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".class")) + .walk( + classFile -> { + Class fileClass = new ClassFileLoader(classFile).findClass(null); + if (fileClass == null) { + return; + } + try { + // Check if class contains {@code public static void main(String[] args)} + Method main = fileClass.getMethod("main", String[].class); + if (main != null + && main.getReturnType() == void.class + && Modifier.isStatic(main.getModifiers()) + && Modifier.isPublic(main.getModifiers())) { + classNames.add(fileClass.getName()); + } + } catch (NoSuchMethodException ignored) { + // main method not found + } + }); + + return classNames; + } + + private MainClassFinder() {} +} diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java index 8d156053bb..efcf1addc2 100644 --- a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/HelpfulSuggestionsTest.java @@ -61,6 +61,9 @@ public void testSuggestions_smoke() { Assert.assertEquals( "messagePrefix, perhaps you should clear directory manually before creating the Docker context", TEST_HELPFUL_SUGGESTIONS.forDockerContextInsecureRecursiveDelete("directory")); + Assert.assertEquals( + "messagePrefix, perhaps you should add a `mainClass` configuration to plugin", + TEST_HELPFUL_SUGGESTIONS.forMainClassNotFound("plugin")); Assert.assertEquals("messagePrefix", TEST_HELPFUL_SUGGESTIONS.none()); } } diff --git a/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java new file mode 100644 index 0000000000..f09a8b0020 --- /dev/null +++ b/jib-core/src/test/java/com/google/cloud/tools/jib/frontend/MainClassFinderTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2018 Google LLC. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.cloud.tools.jib.frontend; + +import com.google.common.io.Resources; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +/** Test for MainClassFinder. */ +public class MainClassFinderTest { + + @Test + public void testFindMainClass_simple() throws URISyntaxException, IOException { + Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/simple").toURI()); + List mainClasses = MainClassFinder.findMainClasses(rootDirectory); + Assert.assertEquals(1, mainClasses.size()); + Assert.assertTrue(mainClasses.contains("HelloWorld")); + } + + @Test + public void testFindMainClass_subdirectories() throws URISyntaxException, IOException { + Path rootDirectory = + Paths.get(Resources.getResource("class-finder-tests/subdirectories").toURI()); + List mainClasses = MainClassFinder.findMainClasses(rootDirectory); + Assert.assertEquals(1, mainClasses.size()); + Assert.assertTrue(mainClasses.contains("multi.layered.HelloWorld")); + } + + @Test + public void testFindMainClass_noClass() throws URISyntaxException, IOException { + Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/no-main").toURI()); + List mainClasses = MainClassFinder.findMainClasses(rootDirectory); + Assert.assertTrue(mainClasses.isEmpty()); + } + + @Test + public void testFindMainClass_multiple() throws URISyntaxException, IOException { + Path rootDirectory = Paths.get(Resources.getResource("class-finder-tests/multiple").toURI()); + List mainClasses = MainClassFinder.findMainClasses(rootDirectory); + Assert.assertEquals(2, mainClasses.size()); + Assert.assertTrue(mainClasses.contains("multi.layered.HelloMoon")); + Assert.assertTrue(mainClasses.contains("HelloWorld")); + } +} diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/HelloWorld.class b/jib-core/src/test/resources/class-finder-tests/multiple/HelloWorld.class new file mode 100644 index 0000000000000000000000000000000000000000..d47291bbb363b84d0f0beeff35270eb955a69ffe GIT binary patch literal 534 zcmZuu%TB^j5IvVyX|3`o;9J2Jy09A)7Z^7t#zmth7+kozzztk-+iObE_*uHp#DyQ= zM;WKZq>|9ZOy``Lb7tD_pU*D z7kVfdN}deF^~~>!@Q(KtAyrQgxa#px#`PbVi^g)wV0%|WDSfX)HSCav&t<^SXg_!p z{={9ygOS^fLK%!Yi3blyH03Oz8LY0Jg#&RZW7ATqdmKyIFi;&Wf}>ej@L6H>hpc2c5i9fh*lQdr-PhCV-aRB9^)oO zm)6_M4n+%j`t0$O0kxw@(WJ_y%qZuNKEPfR+M-xUzzSCA-JUm4UZu*!8cKx3!>5TD fVQDOLMq%8jp|XB4EA`MR}(HX z@^cbVO|~8T!_j{|omsvvVsR^#vD;|R)Nn4^uo+J0@3dAIdjf1$J6ftz#F6XgHU^VUJ=YmvX+E)pkg^I_MuFdBGjz( zf+P}pB~n%Li)iLsl@a>EzR@D}c7V*ZPhdUYNc!a^*WXZU9BBPyAu1T^X#1s aXuxGgXkNp^inDef9NYxy8uZw1p!ElXKRYh~ literal 0 HcmV?d00001 diff --git a/jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer2Class.class b/jib-core/src/test/resources/class-finder-tests/multiple/multi/Layer2Class.class new file mode 100644 index 0000000000000000000000000000000000000000..daf0a2e46ec0c22862c641d1fc0d6fbfb21d6f40 GIT binary patch literal 270 zcmZXO&1%9>5QWd=*QU{EeS+@Pg#qcV#ZACf(1peQO}yw0*97t-^s%}Uy6^#fsNzIi z2+qLAoHKJ^F8_aL03YakFwu3fX4qExNEn^wJ76=4`38eOX}7C*9_MOswVC&616O_|DtYUBNf-82(N;lqMW7(6BU zw%vzX#MiOmIlsF{L%(vvi?}3qQb* zGTs&=lHe@v?LFuA%(-(vzurFp9An=^0qX{;sOhMi*g!+WriLws!l?{objFZvAM_Y< z=V~YzO0EpVb?o;=c*px5Ar)5*xYy&MOzIyn7d^-cL&NoBFOrVOry>+X=Tdl{x>73W zkf6_Hz))-7yJP;usV5jY-6)j7sFPmE!x2f?OK1kOtKx7VE@a|h^;h;NQL>;x*RXA2 z2fGZ#lx_NSYfHnPg(g}I^?!^pl;>`4`eQMO7NG8Q5(%FkMa4u^XV#^1Zpn8DOLMq%8jp|XB4EA`MR}(HX z@^cbVO|~8T!_j{|omsvvVsR^#vD;|R)Nn4^uo+J0@3dAIdjf1$J6ftz#F6XgHU^VUJ=YmvX+E)pkg^I_MuFdBGjz( zf+P}pB~n%Li)iLsl@a>EzR@D}c7V*ZPhdUYNc!a^*WXZU9BBPyAu1T^X#1s aXuxGgXkNp^inDef9NYxy8uZw1p!ElXKRYh~ literal 0 HcmV?d00001 diff --git a/jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer2Class.class b/jib-core/src/test/resources/class-finder-tests/no-main/multi/Layer2Class.class new file mode 100644 index 0000000000000000000000000000000000000000..daf0a2e46ec0c22862c641d1fc0d6fbfb21d6f40 GIT binary patch literal 270 zcmZXO&1%9>5QWd=*QU{EeS+@Pg#qcV#ZACf(1peQO}yw0*97t-^s%}Uy6^#fsNzIi z2+qLAoHKJ^F8_aL03YakFwu3fX4qExNEn^wJ76=4`38eOX}7C*9_MOswVC&616O_|DtYUBNf-82(N;lqMW7(6BU zw%vzXH<8154il(mF==87(*|Ztq%j_aSp#zh<^?ia+SB2- zK+>+C3Z!=YOC^wRYEK>ZyKNPm$hJ$Q*z`NnJ(You>TfU=Ug@5I)!b9A>!107d$~!C zuGF5un0?W_mbbFuO3!Ju!a#e@W;{a%4o{W-p#{vA-w!%!S4Tth1KssV%7Td@12qc^ zSmfN8p1+Ic)(k9JSjLKgap;E!T*1OBY$^^!1V#rN9ks7jC;SDq?t7u?@>}?Q5|yv@ zwcj}A?jap0*`>cytnnM>)SIZurSXdWzi-n~;eqP)q(f_EyZ*NeHDvkQq9*}vjv+>i zRfAbDr`Mh#9%6c!F%yFYjPTw1YCs;Nta2!zNF+MEk7UeMACV{~pO9jHMf&_1nI{;m b2ZAGP1xnna%$rmBAzUR|CP#aW35DOLMq%8jp|XB4EA`MR}(HX z@^cbVO|~8T!_j{|omsvvVsR^#vD;|R)Nn4^uo+J0@3dAIdjf1$J6ftz#F6XgHU^VUJ=YmvX+E)pkg^I_MuFdBGjz( zf+P}pB~n%Li)iLsl@a>EzR@D}c7V*ZPhdUYNc!a^*WXZU9BBPyAu1T^X#1s aXuxGgXkNp^inDef9NYxy8uZw1p!ElXKRYh~ literal 0 HcmV?d00001 diff --git a/jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/HelloWorld.class b/jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/HelloWorld.class new file mode 100644 index 0000000000000000000000000000000000000000..e0a3bb54d2b8a7956a34064421da28cda88aafe6 GIT binary patch literal 562 zcmZ`$O;5r=5Pi#6KdgdO5b+B#)m^ zy9fq?q3B9qT*qEdgm=8B2r0WkpQ|npWm5lwxo9LO40YFwRU{q7ry>*s=SnCQ+y|i= zv`Nw9(r2i)9^5g1;?(31olX==f7niM?JgV*$LvXFD_+bp*Yt)PNXbo&^7E> z*u@@$mC}vp!JxmZ2QJJ}yfpbgNBN{+( zkH)i8jYXJ2drX=X&0T3N8yijJX(h?YfZCx`G^lbYGs^j+cd(a~HYpZT(8daVTeAkr rt5g|SLy?f==hIY-uvcWt**E0o0wz(Q{p+ay5iwB0I^ncliW%5Ht#5g= literal 0 HcmV?d00001 diff --git a/jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/Layer2Class.class b/jib-core/src/test/resources/class-finder-tests/subdirectories/multi/layered/Layer2Class.class new file mode 100644 index 0000000000000000000000000000000000000000..994c983fb49f78608ce76665393139908cbccbab GIT binary patch literal 286 zcmZ`!%WA?<5Iqy~s4;ECuEdo)bzwldQ@Rnl3c66--^5FMkm~CMse2YT18rX$xZi>g`@{9VzMseDt$rhhk^pTwsK zV8bB{|8?LSv&ZK7d$dhXGOaO&#Xp!JPBvh4nMExJoY@cPn*|FVqsubzj0eV`?HzRC KGb8jEEd;+kLOo3Y literal 0 HcmV?d00001 diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle index f801759553..d6c59b7ae9 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/empty/build.gradle @@ -15,7 +15,6 @@ jib { image = 'gcr.io/jib-integration-testing/emptyimage:gradle' credHelper = 'gcr' } - mainClass = 'com.test.Empty' // Does not have tests use user-level cache for base image layers. useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle index 5024f49272..763ba021b9 100644 --- a/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle +++ b/jib-gradle-plugin/src/integration-test/resources/projects/simple/build.gradle @@ -19,7 +19,6 @@ jib { image = 'gcr.io/jib-integration-testing/simpleimage:gradle' credHelper = 'gcr' } - mainClass = 'com.test.HelloWorld' // Does not have tests use user-level cache for base image layers. useOnlyProjectCache = true } diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java index c2e2bdf641..38fbcb9c5d 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildDockerTask.java @@ -89,7 +89,8 @@ public void buildDocker() throws InvalidImageReferenceException { + "' does not use a specific image digest - build may not be reproducible"); } - ProjectProperties projectProperties = new ProjectProperties(getProject(), gradleBuildLogger); + ProjectProperties projectProperties = + ProjectProperties.getForProject(getProject(), gradleBuildLogger); String mainClass = projectProperties.getMainClass(jibExtension.getMainClass()); RegistryCredentials knownBaseRegistryCredentials = null; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java index abdcc0f905..b694ea1f5a 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/BuildImageTask.java @@ -95,7 +95,8 @@ public void buildImage() throws InvalidImageReferenceException { + "' does not use a specific image digest - build may not be reproducible"); } - ProjectProperties projectProperties = new ProjectProperties(getProject(), gradleBuildLogger); + ProjectProperties projectProperties = + ProjectProperties.getForProject(getProject(), gradleBuildLogger); String mainClass = projectProperties.getMainClass(jibExtension.getMainClass()); RegistryCredentials knownBaseRegistryCredentials = null; diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java index ce295c055f..8a09731492 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/DockerContextTask.java @@ -65,7 +65,8 @@ public void generateDockerContext() { GradleBuildLogger gradleBuildLogger = new GradleBuildLogger(getLogger()); - ProjectProperties projectProperties = new ProjectProperties(getProject(), gradleBuildLogger); + ProjectProperties projectProperties = + ProjectProperties.getForProject(getProject(), gradleBuildLogger); String mainClass = projectProperties.getMainClass(jibExtension.getMainClass()); String targetDir = getTargetDir(); diff --git a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ProjectProperties.java b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ProjectProperties.java index 92d2625b51..5b0233ae71 100644 --- a/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ProjectProperties.java +++ b/jib-gradle-plugin/src/main/java/com/google/cloud/tools/jib/gradle/ProjectProperties.java @@ -18,7 +18,10 @@ import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import com.google.cloud.tools.jib.frontend.MainClassFinder; +import com.google.common.annotations.VisibleForTesting; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; @@ -30,45 +33,91 @@ /** Obtains information about a Gradle {@link Project} that uses Jib. */ class ProjectProperties { + private static final String PLUGIN_NAME = "jib"; + private final Project project; private final GradleBuildLogger gradleBuildLogger; + private final SourceFilesConfiguration sourceFilesConfiguration; + + /** @return a ProjectProperties from the given project and logger. */ + static ProjectProperties getForProject(Project project, GradleBuildLogger gradleBuildLogger) { + try { + return new ProjectProperties( + project, gradleBuildLogger, GradleSourceFilesConfiguration.getForProject(project)); + } catch (IOException ex) { + throw new GradleException("Obtaining project build output files failed", ex); + } + } - ProjectProperties(Project project, GradleBuildLogger gradleBuildLogger) { + @VisibleForTesting + ProjectProperties( + Project project, + GradleBuildLogger gradleBuildLogger, + SourceFilesConfiguration sourceFilesConfiguration) { this.project = project; this.gradleBuildLogger = gradleBuildLogger; + this.sourceFilesConfiguration = sourceFilesConfiguration; } /** - * @param mainClass the configured main class - * @return the main class to use for the container entrypoint. + * If {@code mainClass} is {@code null}, tries to infer main class in this order: + * + *
    + *
  • 1. Looks in a {@code jar} task. + *
  • 2. Searches for a class defined with a main method. + *
+ * + *

Warns if main class is not valid. + * + * @throws GradleException if no valid main class is not found. */ String getMainClass(@Nullable String mainClass) { if (mainClass == null) { + gradleBuildLogger.info( + "Searching for main class... Add a 'mainClass' configuration to '" + + PLUGIN_NAME + + "' to improve build speed."); mainClass = getMainClassFromJarTask(); if (mainClass == null) { - throw new GradleException( - HelpfulSuggestionsProvider.get("Could not find main class specified in a 'jar' task") - .suggest("add a `mainClass` configuration to jib")); + gradleBuildLogger.debug( + "Could not find main class specified in a 'jar' task; attempting to " + + "infer main class."); + try { + // Adds each file in each classes output directory to the classes files list. + List mainClasses = new ArrayList<>(); + for (Path classPath : sourceFilesConfiguration.getClassesFiles()) { + mainClasses.addAll(MainClassFinder.findMainClasses(classPath)); + } + + if (mainClasses.size() == 1) { + mainClass = mainClasses.get(0); + } else if (mainClasses.size() == 0) { + throw new GradleException( + HelpfulSuggestionsProvider.get("Main class was not found") + .forMainClassNotFound(PLUGIN_NAME)); + } else { + throw new GradleException( + HelpfulSuggestionsProvider.get( + "Multiple valid main classes were found: " + String.join(", ", mainClasses)) + .forMainClassNotFound(PLUGIN_NAME)); + } + } catch (IOException ex) { + throw new GradleException( + HelpfulSuggestionsProvider.get("Failed to get main class") + .forMainClassNotFound(PLUGIN_NAME), + ex); + } } } if (!BuildConfiguration.isValidJavaClass(mainClass)) { - getLogger().warn("'mainClass' is not a valid Java class : " + mainClass); + gradleBuildLogger.warn("'mainClass' is not a valid Java class : " + mainClass); } return mainClass; } - GradleBuildLogger getLogger() { - return gradleBuildLogger; - } - /** @return the {@link SourceFilesConfiguration} based on the current project */ SourceFilesConfiguration getSourceFilesConfiguration() { - try { - return GradleSourceFilesConfiguration.getForProject(project); - - } catch (IOException ex) { - throw new GradleException("Obtaining project build output files failed", ex); - } + return sourceFilesConfiguration; } /** Extracts main class from 'jar' task, if available. */ diff --git a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/ProjectPropertiesTest.java b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/ProjectPropertiesTest.java index 8606fee3df..2685238919 100644 --- a/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/ProjectPropertiesTest.java +++ b/jib-gradle-plugin/src/test/java/com/google/cloud/tools/jib/gradle/ProjectPropertiesTest.java @@ -16,7 +16,11 @@ package com.google.cloud.tools.jib.gradle; +import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; +import java.util.List; import org.gradle.api.GradleException; import org.gradle.api.Project; import org.gradle.api.internal.file.FileResolver; @@ -42,7 +46,9 @@ public class ProjectPropertiesTest { @Mock private Jar mockJar; @Mock private Project mockProject; @Mock private GradleBuildLogger mockGradleBuildLogger; + @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; + private final List classesPath = Collections.singletonList(Paths.get("a/b/c")); private Manifest fakeManifest; private ProjectProperties testProjectProperties; @@ -50,8 +56,10 @@ public class ProjectPropertiesTest { public void setUp() { fakeManifest = new DefaultManifest(mockFileResolver); Mockito.when(mockJar.getManifest()).thenReturn(fakeManifest); + Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(classesPath); - testProjectProperties = new ProjectProperties(mockProject, mockGradleBuildLogger); + testProjectProperties = + new ProjectProperties(mockProject, mockGradleBuildLogger, mockSourceFilesConfiguration); } @Test @@ -95,9 +103,9 @@ private void assertGetMainClassFails() { Assert.fail("Main class not expected"); } catch (GradleException ex) { - Assert.assertThat( - ex.getMessage(), - CoreMatchers.containsString("Could not find main class specified in a 'jar' task")); + Mockito.verify(mockGradleBuildLogger) + .debug( + "Could not find main class specified in a 'jar' task; attempting to infer main class."); Assert.assertThat( ex.getMessage(), CoreMatchers.containsString("add a `mainClass` configuration to jib")); } diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java index 2762cd63a2..fb97638c23 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildDockerMojo.java @@ -57,7 +57,7 @@ public class BuildDockerMojo extends JibPluginConfiguration { /** TODO: Consolidate with BuildImageMojo. */ @Override public void execute() throws MojoExecutionException, MojoFailureException { - ProjectProperties projectProperties = new ProjectProperties(getProject(), getLog()); + ProjectProperties projectProperties = ProjectProperties.getForProject(getProject(), getLog()); String inferredMainClass = projectProperties.getMainClass(getMainClass()); SourceFilesConfiguration sourceFilesConfiguration = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java index c4ca4a5914..e951b617fe 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/BuildImageMojo.java @@ -77,7 +77,7 @@ private Class getManifestTemplateClass() { public void execute() throws MojoExecutionException, MojoFailureException { validateParameters(); - ProjectProperties projectProperties = new ProjectProperties(getProject(), getLog()); + ProjectProperties projectProperties = ProjectProperties.getForProject(getProject(), getLog()); String inferredMainClass = projectProperties.getMainClass(getMainClass()); SourceFilesConfiguration sourceFilesConfiguration = diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java index a132ea0198..c7a1727e8b 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/DockerContextMojo.java @@ -43,7 +43,7 @@ public class DockerContextMojo extends JibPluginConfiguration { public void execute() throws MojoExecutionException { Preconditions.checkNotNull(targetDir); - ProjectProperties projectProperties = new ProjectProperties(getProject(), getLog()); + ProjectProperties projectProperties = ProjectProperties.getForProject(getProject(), getLog()); String inferredMainClass = projectProperties.getMainClass(getMainClass()); try { diff --git a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProjectProperties.java b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProjectProperties.java index 1dd46de3d9..69b920670a 100644 --- a/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProjectProperties.java +++ b/jib-maven-plugin/src/main/java/com/google/cloud/tools/jib/maven/ProjectProperties.java @@ -18,8 +18,13 @@ import com.google.cloud.tools.jib.builder.BuildConfiguration; import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import com.google.cloud.tools.jib.frontend.MainClassFinder; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import javax.annotation.Nullable; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.MojoExecutionException; @@ -30,19 +35,18 @@ /** Obtains information about a {@link MavenProject}. */ class ProjectProperties { + private static final String PLUGIN_NAME = "jib-maven-plugin"; + private final MavenProject project; private final Log log; + private final SourceFilesConfiguration sourceFilesConfiguration; - ProjectProperties(MavenProject project, Log log) { - this.project = project; - this.log = log; - } - - /** @return the {@link SourceFilesConfiguration} based on the current project */ - SourceFilesConfiguration getSourceFilesConfiguration() throws MojoExecutionException { + /** @return a ProjectProperties from the given project and logger. */ + static ProjectProperties getForProject(MavenProject project, Log log) + throws MojoExecutionException { try { - return MavenSourceFilesConfiguration.getForProject(project); - + return new ProjectProperties( + project, log, MavenSourceFilesConfiguration.getForProject(project)); } catch (IOException ex) { throw new MojoExecutionException( "Obtaining project build output files failed; make sure you have compiled your project " @@ -52,15 +56,67 @@ SourceFilesConfiguration getSourceFilesConfiguration() throws MojoExecutionExcep } } - /** @return the main class to use in the container entrypoint */ + @VisibleForTesting + ProjectProperties( + MavenProject project, Log log, SourceFilesConfiguration sourceFilesConfiguration) { + this.project = project; + this.log = log; + this.sourceFilesConfiguration = sourceFilesConfiguration; + } + + /** @return the {@link SourceFilesConfiguration} based on the current project */ + SourceFilesConfiguration getSourceFilesConfiguration() { + return sourceFilesConfiguration; + } + + /** + * If {@code mainClass} is {@code null}, tries to infer main class in this order: + * + *

    + *
  • 1. Looks in a {@code jar} task. + *
  • 2. Searches for a class defined with a main method. + *
+ * + *

Warns if main class is not valid. + * + * @throws MojoExecutionException if no valid main class is not found. + */ String getMainClass(@Nullable String mainClass) throws MojoExecutionException { if (mainClass == null) { + log.info( + "Searching for main class... Add a 'mainClass' configuration to '" + + PLUGIN_NAME + + "' to improve build speed."); mainClass = getMainClassFromMavenJarPlugin(); if (mainClass == null) { - throw new MojoExecutionException( - HelpfulSuggestionsProvider.get( - "Could not find main class specified in maven-jar-plugin") - .suggest("add a `mainClass` configuration to jib-maven-plugin")); + log.debug( + "Could not find main class specified in maven-jar-plugin; attempting to infer main " + + "class."); + + try { + List mainClasses = new ArrayList<>(); + for (Path classPath : sourceFilesConfiguration.getClassesFiles()) { + mainClasses.addAll(MainClassFinder.findMainClasses(classPath)); + } + + if (mainClasses.size() == 1) { + mainClass = mainClasses.get(0); + } else if (mainClasses.size() == 0) { + throw new MojoExecutionException( + HelpfulSuggestionsProvider.get("Main class was not found") + .forMainClassNotFound(PLUGIN_NAME)); + } else { + throw new MojoExecutionException( + HelpfulSuggestionsProvider.get( + "Multiple valid main classes were found: " + String.join(", ", mainClasses)) + .forMainClassNotFound(PLUGIN_NAME)); + } + } catch (IOException ex) { + throw new MojoExecutionException( + HelpfulSuggestionsProvider.get("Failed to get main class") + .forMainClassNotFound(PLUGIN_NAME), + ex); + } } } Preconditions.checkNotNull(mainClass); diff --git a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProjectPropertiesTest.java b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProjectPropertiesTest.java index 37f00d851b..94ae59cd4c 100644 --- a/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProjectPropertiesTest.java +++ b/jib-maven-plugin/src/test/java/com/google/cloud/tools/jib/maven/ProjectPropertiesTest.java @@ -16,6 +16,12 @@ package com.google.cloud.tools.jib.maven; +import com.google.cloud.tools.jib.builder.SourceFilesConfiguration; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import org.apache.maven.model.Build; import org.apache.maven.model.Plugin; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.logging.Log; @@ -37,9 +43,13 @@ public class ProjectPropertiesTest { @Mock private MavenProject mockMavenProject; @Mock private Log mockLog; @Mock private Plugin mockJarPlugin; + @Mock private SourceFilesConfiguration mockSourceFilesConfiguration; + + @Mock private Build mockBuild; private final Xpp3Dom fakeJarPluginConfiguration = new Xpp3Dom(""); private final Xpp3Dom jarPluginMainClass = new Xpp3Dom("mainClass"); + private final List classesPath = Collections.singletonList(Paths.get("a/b/c")); private ProjectProperties testProjectProperties; @@ -52,8 +62,10 @@ public void setUp() { manifest.addChild(jarPluginMainClass); Mockito.when(mockJarPlugin.getConfiguration()).thenReturn(fakeJarPluginConfiguration); + Mockito.when(mockSourceFilesConfiguration.getClassesFiles()).thenReturn(classesPath); - testProjectProperties = new ProjectProperties(mockMavenProject, mockLog); + testProjectProperties = + new ProjectProperties(mockMavenProject, mockLog, mockSourceFilesConfiguration); } @Test @@ -97,9 +109,9 @@ private void assertGetMainClassFails() { Assert.fail("Main class not expected"); } catch (MojoExecutionException ex) { - Assert.assertThat( - ex.getMessage(), - CoreMatchers.containsString("Could not find main class specified in maven-jar-plugin")); + Mockito.verify(mockLog) + .debug( + "Could not find main class specified in maven-jar-plugin; attempting to infer main class."); Assert.assertThat( ex.getMessage(), CoreMatchers.containsString("add a `mainClass` configuration to jib-maven-plugin")); diff --git a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml index 275cf5c7c8..3e301cbb2e 100644 --- a/jib-maven-plugin/src/test/resources/projects/empty/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/empty/pom.xml @@ -31,7 +31,6 @@ gcr.io/jib-integration-testing/emptyimage:maven - com.test.Empty true diff --git a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml index ac581903a0..34f54f7261 100644 --- a/jib-maven-plugin/src/test/resources/projects/simple/pom.xml +++ b/jib-maven-plugin/src/test/resources/projects/simple/pom.xml @@ -41,7 +41,6 @@ gcr.io/jib-integration-testing/simpleimage:maven - com.test.HelloWorld true