Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infer main class if it isn't configured anywhere #278

Merged
merged 12 commits into from
May 17, 2018
Merged

Conversation

TadCordle
Copy link
Contributor

Searches the build output for .class files with a main method. If more than one is found, or if one isn't found, it throws an error.

Fixes #189.

@TadCordle TadCordle requested review from loosebazooka and coollog May 16, 2018 19:35
Copy link
Contributor

@coollog coollog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good stuff!

import java.util.stream.Stream;
import javax.annotation.Nullable;

/** Class used for inferring the main class in an application. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: usually can omit phrases like Class used for so it's like Infers the main...

/** Helper class for loading a .class file. */
private static class ClassFileLoader extends ClassLoader {

private Path classFile;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final

}

/**
* Searches for a class containing a main method given an absolute root directory.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should prob have a note that this is searching for .class files in the directory.

}

String className = null;
try (Stream<Path> pathStream = Files.walk(Paths.get(rootDirectory))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can use DirectoryWalker here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this much of an improvement? Looks like implementing a DirectoryWalker would be more complicated.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mostly as a way to abstract away the strategy for walking into a testable class. The use would like something like:

new DirectoryWalker(rootDirectory).filter(Files::isRegularFile).filter(path -> path.toString().endsWith(".class")).walk(classFile -> {...});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, the one you implemented. I was thinking you meant something like this https://commons.apache.org/proper/commons-io/javadocs/api-1.4/org/apache/commons/io/DirectoryWalker.html

List<Path> classFiles =
pathStream
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".class"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path has endsWith

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right that makes sense. Looks like we used Filesystems#getPathMatcher in MavenSourceFilesConfiguration.

@@ -15,7 +15,6 @@ jib {
image = 'gcr.io/jib-integration-testing/emptyimage:gradle'
credHelper = 'gcr'
}
mainClass = 'com.test.Empty'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😃

throw new GradleException(
HelpfulSuggestionsProvider.get("Could not find main class specified in a 'jar' task")
.suggest("add a `mainClass` configuration to jib"));
gradleBuildLogger.info(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could prob just be at the debug level.

try {
mainClass =
MainClassFinder.findMainClass(
project
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use GradleSourceConfiguration#getClassesFiles, which will be the files that go into the container.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns a list of Paths, how would I use it to get the root directory?

} catch (MultipleClassesFoundException | IOException ex) {
throw new GradleException(
HelpfulSuggestionsProvider.get("Failed to get main class: " + ex.getMessage())
.suggest("add a `mainClass` configuration to jib"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ex should go as a second argument to GradleException rather than a string addition so there is stack hierarchy.

} catch (MultipleClassesFoundException | IOException ex) {
throw new GradleException(
HelpfulSuggestionsProvider.get("Failed to get main class: " + ex.getMessage())
.suggest("add a `mainClass` configuration to jib"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This suggestion can be a constant together with below.

@coollog
Copy link
Contributor

coollog commented May 16, 2018 via email

public static String findMainClass(String rootDirectory)
throws MultipleClassesFoundException, IOException {
public static List<String> findMainClass(Path rootDirectory) throws IOException {
List<String> results = new ArrayList<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more descriptive variable name like classNames

// Got an invalid class file, keep searching
continue;
}
// Get all .class files
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Get/Searches (or Looks at)

.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".class"))
.walk(
classFile -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to put this in a separate method since it is getting to almost 6 levels of tabbing around line 91.

&& main.getReturnType() == void.class
&& Modifier.isStatic(main.getModifiers())
&& Modifier.isPublic(main.getModifiers())) {
results.add(name);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is it possible to not have to do the path-to-class conversion and instead just get the class from the file via reflection?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't look like it, I think I need to pass the full name into the class loader.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe if you pass in null to defineClass it would resolve the class name from the file.

}
}
Class<?> fileClass = new ClassFileLoader(classFile).findClass(name);
if (fileClass != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An early return here would help reduce the tabbing.

"Could not find main class specified in a 'jar' task; attempting to "
+ "infer main class.");

final String mainClassSuggestion = "add a `mainClass` configuration to jib";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh by constant, I meant like a private static final field.

}
if (mainClass == null) {
// Adds each file in each classes output directory to the classes files list.
JavaPluginConvention javaPluginConvention =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I meant here to just directly use the classes files from SourceFilesConfiguration

Copy link
Contributor Author

@TadCordle TadCordle May 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was either this or something like getSourceFilesConfiguration().getClassesFiles().get(0).getParent() since I need the root directory to resolve the class names correctly, and getClassesFiles seems to return a list of the files in the root directory. Plus using getSourceFilesConfiguration() was weird in testing, since I would either need to refactor a bit, or mock a bunch of the GradleSourceFilesConfiguration internals to avoid null pointer exceptions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, though we do need some way to guarantee that the classes we are searching in are the classes that will be in the container.

project.getConvention().getPlugin(JavaPluginConvention.class);
SourceSet mainSourceSet = javaPluginConvention.getSourceSets().getByName("main");
Path classesDirs = Paths.get(mainSourceSet.getOutput().getClassesDirs().getAsPath());
List<String> mainClasses = new ArrayList<>(MainClassFinder.findMainClass(classesDirs));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new ArrayList<> doesn't seem to be necessary.

mainClass = mainClasses.get(0);
} else if (mainClasses.size() == 0) {
throw new GradleException(
HelpfulSuggestionsProvider.get("Main class was not found")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these messages are the same as in Maven, they can be added to HelpfulSuggestions.

@@ -48,29 +51,36 @@ String getMainClass(@Nullable String mainClass) {
if (mainClass == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the logic is getting a bit more complex, it'd be good to add a description to the Javadoc like:

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.

Copy link
Contributor

@coollog coollog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there!

&& main.getReturnType() == void.class
&& Modifier.isStatic(main.getModifiers())
&& Modifier.isPublic(main.getModifiers())) {
classNames.add(fileClass.getCanonicalName());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you would want getName since the canonical name would replace a subclass like package.Hello$Main with package.Hello.Main

https://stackoverflow.com/questions/15202997/what-is-the-difference-between-canonical-name-simple-name-and-class-name-in-jav

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

K, wasn't sure which one we wanted.


ProjectProperties(Project project, GradleBuildLogger gradleBuildLogger) {
this.project = project;
this.gradleBuildLogger = gradleBuildLogger;
try {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good!

This should prob go in a static method like ProjectProperties#getForProject since constructors generally should be lightweight.

@@ -54,32 +78,29 @@ String getMainClass(@Nullable String mainClass) {
gradleBuildLogger.debug(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was talking to @patflynn and Patrick mentioned that it might be better to log something like Searching for main class... Add a 'mainClass' configuration to 'jib' to improve build speed. at the info level to help users in case they have a ton of classes to search.

try {
List<String> mainClasses =
MainClassFinder.findMainClass(Paths.get(project.getBuild().getOutputDirectory()));
MainClassFinder.findMainClasses(Paths.get(project.getBuild().getOutputDirectory()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should use MavenSourceFilesConfiguration like you did in Gradle.

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 "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a todo to consolidate this code with the Maven one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already added it to #262, but I can add a TODO in the code as well if you want.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay that's fine then.

@TadCordle TadCordle merged commit ce40f40 into master May 17, 2018
@TadCordle TadCordle deleted the infer_main_class branch May 17, 2018 18:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants