Load classpath of project gradle plugin is applied to - java

I am writing a custom gradle plugin, which will generate some code for me, based on the code it finds in the project it is applied to.
For this I need to find all classes that extend a specific class.
The problem is that the class, that is extended, is not loaded in the classpath, since it is a dependency of the other project.
Currently I got this for my custom Task
public class GenerateCodeTask extends DefaultTask {
#TaskAction
public void generateCode() throws MalformedURLException, ClassNotFoundException {
File buildDir = new File(getProject().getBuildDir(), "classes/main");
File root = new File(getProject().getProjectDir(), "src/main/generated");
URLClassLoader classLoader = new URLClassLoader(new URL[]{buildDir.toURL()});
Class itemClass = classLoader.loadClass("net.minecraft.item.Item");
Reflections reflections = new Reflections(classLoader);
Set<Class<?>> items = reflections.getSubTypesOf(itemClass);
}
}
And this for the plugin
public class EasymodsPlugin implements Plugin<Project> {
#Override
public void apply(Project p) {
Task t = p.getTasks().create("generateCode", GenerateCodeTask.class);
t.dependsOn(p.getTasks().getByPath("compileJava"));
}
}
This is the error I am getting
java.lang.ClassNotFoundException: net.minecraft.item.Item
I know that the problem is that the library containing the class is not loaded, and that causes the exception.
What I want is to be able to load all dependencies of my project into the classloader, so I can use reflections to find all "items" in the project (which I need to generate code)
Greetings Failender

I think you almost got it.
You need the compileClasspath property. I pass it as an input parameter to my task, and build the Class Loader from it:
In plugin:
Set<File> ccp = project.getConfigurations().getByName("compileClasspath").getFiles();
task.classpath = ccp;
In task:
#InputFiles
Iterable<File> classpath;
What this property is adding is:
the path to the classes on projects it depends on that are generated under project_id/build dir. ( So no need to build the path manually )
All library deps for your project. That i think you were missing.
So, in the case your class is of type 1:
It is better if you have different subprojects: the one that has the original classes and the other with the generated ones.
So the second depends on the first:
dependencies {
compile project(':my_project_with_classes_to_extend')
}
In the case is the second one, you can just add the library as a dep to your project and it will find the class.
And rewire the tasks so you are sure the fist project is built before calling your task (just on root level of your 2nd project build.gradle):
I think that´s the part that wasn't really working for you, apparently, compileJava and build are not exactly the same. Or at least, compileJava wasnt working for me either.
myGeneratorTask.dependsOn( ":my_project_with_classes_to_extend:build" )
compileJava.dependsOn( "myGeneratorTask" )

Related

Getting the resource path of the java project from a custom gradle plugin

Trying to create a custom gradle plugin in java, how do i get the resources path from inside the task class?
public class MyCustomPlugin implements Plugin<Project> {
#Override
public void apply(Project project) {
project.getTasks().register("doStuff", CustomTask.class);
}
}
public class CustomTask extends DefaultTask {
// How do I get java project resources dir from here?
#Inject
public CustomTask(ProjectLayout projectLayout) {
directoryProperty = projectLayout.getBuildDirectory();
}
#TaskAction
public void execute() {
...
}
}
I would recommend to not get the directory inside the task, because the plugin that provides it might not be applied. Instead I would do it from within your plugin that registers the task, this way you can also ensure that the necessary plugin is actually applied. Gradle will display an error if the task is used without a value being assigned to the input that explains that nothing was assigned.
With the kotlin-dsl:
#CacheableTask
abstract class CustomTask : DefaultTask() {
#get:InputFiles
abstract val resources: FileCollection
//...
}
I cannot answer if #InputFiles is the right annotation for your use case, because I don't know what you want to do with the resource. Refer to the Gradle documentation for more information on the available annotations, and what they do.
plugins {
java
}
tasks.register<CustomTask>("customTask") {
resources.set(sourceSets.main.map { it.resources })
}
Notice the map {} which ensures that our task has a dependency on the processResources task, this is done automatically for us because we stick to the provider API of Gradle for everything.
Note that the resources are by default in one directory, but they don't have to be. This is why the resources are defined as SourceDirectorySet and not as Provider<Directory>. The same is true for anything that originates from the SourceSetContainer. It is easier to explain with Java source code: imagine you have Java and Kotlin, then you will have src/main/java and src/main/kotlin, hence, 2 directories. The former will have a **/*.java include filter, whereas the latter has a **/*.kt includes filter. If we just want to get all sources then we use sourceSets.main.map { it.java.sourceDirectories }, and if we want to get one of both it gets complicated. 😝
First, you'd have to ensure this is a Java project: either applying the "java" plugin from your plugin (project.getPluginManager().apply("java")), or only registering the task when the "java" plugin has been applied by the user (project.getPluginManager().withPlugin("java", ignored -> { project.getTasks().register(…); });).
You could then get the resources from the main source set:
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
// Use named() instead of get() if you prefer/need to use providers
SourceSet mainSourceSet = sourceSets.get(SourceSet.MAIN_SOURCE_SET_NAME);
SourceDirectorySet resources = mainSourceSet.getResources();
BTW, the best practice is to have tasks only declare their inputs and outputs (e.g. I need a set of directories, or files, as inputs, and my outputs will be one single file, or in one single directory) and have the actual wiring with default values be done by the plugin.
You could have the plugin unconditionally register the task, then conditionally when the "java" plugin is applied configure its inputs to the project resources; or conditionally register the task or unconditionally apply the "java" plugin, as I showed above.
You can access the sources through the project.sourceSets.
#Inject
public CustomTask(Project project) {
directoryProperty = project.projectLayout.getBuildDirectory();
sourceSet = project.sourceSets.main
}
See also the reference documentation here: https://docs.gradle.org/current/userguide/java_plugin.html#sec:java_project_layout

ServiceLoader does not load implementation jars

I am trying to implement a simple modular application with a self written plugin loader. For that I created a gradle multiproject build. The main project is going to be the started application. In the subproject spi is a module containing the Plugin interface, while in the subproject simple-plugin I wanted to create a plugin to test the plugin loading.
The Plugin interface is very simple:
package io.github.zeroneca.orgascreen.spi;
public interface Plugin {
String getId();
}
module-info.java:
module io.github.zeroneca.orgascreen.spi {
exports io.github.zeroneca.orgascreen.spi;
}
SimplePlugin.java:
package io.github.zeroneca.orgascreen.plugins.simple;
import io.github.zeroneca.orgascreen.spi.Plugin;
public class SimplePlugin implements Plugin {
private final String id = "SimplePlugin";
#Override
public String getId() { return id; }
}
module-info.java
module io.github.zeroneca.orgascreen.plugins.simple {
requires io.github.zeroneca.orgascreen.spi;
provides io.github.zeroneca.orgascreen.spi.Plugin with io.github.zeroneca.orgascreen.plugins.simple.SimplePlugin;
}
Now in the class PluginLoader I'm loading all jars from a directory and want to add all Plugins to a List:
package io.github.zeroneca.orgascreen;
// several imports
public class PluginLoader {
private static final List<Plugin> PLUGINS = new ArrayList<>();
public static void load() {
URL[] pluginURLs = getPluginURLs(); // returns all URLs of jar files in a directory
URLClassLoader urlClassLoader = URLClassLoader.newInstance(pluginURLs);
ServiceLoader<Plugin> serviceLoader = ServiceLoader.load(Plugin.class, urlClassLoader);
for (Plugin plugin : serviceLoader) {
PLUGINS.add(plugin);
}
}
// some more methods hidden here
}
module-info.java
module io.github.zeroneca.orgascreen {
requires io.github.zeroneca.orgascreen.spi;
uses io.github.zeroneca.orgascreen.spi.Plugin;
}
Now after building with gradle and copying the resulting jar simple-plugin-0.1.jar into the directory, I can see, that the URL of the file is in pluginJars but the List PLUGINS remains empty. I'm not sure what I'm doing wrong, online searches didn't help, I cannot see a difference to working solution, but the use of gradle.
gradle 6.5.1 is in use and modularity.inferModulePath is set to true.
Help is greatly appreciated!
Note: when adding META-INF/services/io.github.zeroneca.orgascreen.spi.Plugin to the jar of SimplePlugin it works, so something must be wrong with the module declarations. But I just can't see what.

SPI with Guice error

So I've been making some kind of plugins API for a Java project (to load JAR files externally) and well, I wanted to be able to add any Guice module inside any plugin to my project's dependency graph.
What I did was have a PluginsModule and in the configure method scan for other modules in plugins and install them using Java's ServiceLoader.
I made a test plugin and made a module for it, I confirmed it did get installed. No problems at this point. The problems appear when I do anything inside that module, for example I bound some interface to an implementation in that plugin (just to clear this up, I did the same thing without the plugin and it worked so it's not a binding problem) and tried to inject it, configuration errors saying there was no implementation for that interface appear.
public enum StandardGuiceModuleScanningStrategy implements GuiceModuleScanningStrategy {
INSTANCE;
#Override
public Set<Module> scan(Path directory) throws IOException {
File directoryAsFile = directory.toFile();
File[] childrenFiles = directoryAsFile.listFiles();
if (!directoryAsFile.isDirectory()
|| childrenFiles == null
|| childrenFiles.length == 0) {
return Collections.emptySet();
}
Set<Module> modules = new HashSet<>();
for (File childrenFile : childrenFiles) {
ClassLoader directoryClassLoader = new URLClassLoader(
new URL[]{childrenFile.toURI().toURL()});
ServiceLoader<Module> moduleServiceLoader = ServiceLoader.load(
Module.class, directoryClassLoader);
moduleServiceLoader.forEach(modules::add);
}
return modules;
}
In that implementation of my GuiceModuleScanningStrategy, as I mentioned before, I did use ServiceLoader. Anyways, I also tried other stuff, like scanning the JAR file and checking for a Module, and seeing if it has a specific annotation.
All Guice Modules annotated with #GuiceModule, will be installed into a child Injector. All classes annotated with #AutoBind will be bound to all inherited interfaces. You can also name it, which would lead to a named binding and overwrite the interfaces, which should be used. And if you don't want to use all Features, just overwrite the StartupModule and bind only the Features you want or your own.

JUnitCore giving "No runnable methods" error in Eclipse plugin

I am trying to write an Eclipse plugin that can run JUnit tests and do something with the results. My plugin loads a given class correctly, but fails to run the JUnit tests and gives an error: initializationError(className): No runnable methods. When I run the test class using Result result = JUnitCore.runClasses(className.class); Failure failure : result.getFailures(); from within the same Eclipse instance, however, I don't get any errors.
I think my problem is the one that #gubby describes in the question java.lang.Exception: No runnable methods exception in running JUnits, but I don't know how to implement his suggestion to a solution which reads: "Solution is to load JUnitCore in the same ClassLoader as the tests themselves."
Here is a reduced version of my implementation (please assume that everything except the loading of the runnable methods work):
ClassLoader classLoader = ClassLoaderHelper.getClassLoader(FileFinder.getCurrentProject());
Class clazz = classLoader.loadClass(fileName.substring(0, fileName.indexOf(".class")));
Result result = JUnitCore.runClasses(clazz);
Failure failure : result.getFailures()
The code to get the ClassLoader is the following:
public static ClassLoader getClassLoader(IProject project) {
String[] classPathEntries = null;
try {
project.open(null);
IJavaProject javaProject = JavaCore.create(project);
classPathEntries = JavaRuntime.computeDefaultRuntimeClassPath(javaProject);
} catch (CoreException e1) {
e1.printStackTrace();
}
List<URL> urlList = new ArrayList<URL>();
for (String entry : classPathEntries) {
IPath path = new Path(entry);
URL url;
try {
url = path.toFile().toURI().toURL();
urlList.add(url);
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
ClassLoader parentClassLoader = project.getClass().getClassLoader();
URL[] urls = (URL[]) urlList.toArray(new URL[urlList.size()]);
URLClassLoader classLoader = new URLClassLoader(urls, parentClassLoader);
return classLoader;
}
For Eclipse plugins, you have basically two options to have one plugin share a class loader with another.
Buddy Class Loading. Note that this breaks loose coupling, but it's easy to "implement" as you simply add two statements in the two respective plugins' MANIFEST.MF and an export statement as well. The following rules apply (from the link given above).
The bundle Y must specify the registered buddy policy (i.e. Eclipse-BuddyPolicy: registered)
The bundle X must specify the symbolic name of Y in the Eclipse-RegisterBuddy header (i.e Eclipse-RegisterBuddy: Y)
The bundle X must be dependent on a package exported by bundle Y. This can happen through either a Require-Bundle or Import-Package constraint.
Fragments: You can "attach" a fragment to a plugin. Both share the same class loader. Usually, this technique is used for things like plugin i18n, but it's also the recommended practice for adding unit tests to plugins. This way, the tests don't have to go into the same plugin, and possibly unneeded test classes or dependencies won't go in the production code.
There's a wizard for fragments in Eclipse, but they're basically plugins themselves, which declare a "host plugin".
So, you could consider putting your code into a fragment and attach that to the respective plugin containing the code-under-test. Or, if you need to reuse your code for different plugins and do not care about loose coupling, use Buddy Class Loading.
Also, check whether you have all the right dependencies in the plugin (e.g., org.junit). You need the JUnit contained in the Java Development Tools.

Correct way to define a Gradle plugin property extension with Java?

I'm trying to create a Gradle plugin in Java that has property extensions (not conventions, as this is apparently the old, wrong way). For the record, I'm working with Gradle 1.6 on a Linux machine (Ubuntu 12.04).
I've gotten as far as figuring out that the this should be done in the Plugin class definition. Here is A way of adding an extension. Create an extension class that contains your properties:
public class MyPluginExtensions {
File sourceDir;
File outputDir;
public MyPluginExtensions(Project project) {
this.project = project;
sourceDir = new File(project.getProjectDir(), "src");
outputDir = new File(project.getBuildDir(), "out");
}
}
Now add these extensions to the project in the main plugin class:
public class MyPlugin implements Plugin<Project> {
#Override
public void apply(Project project) {
Map<String,Object> taskInfo = new HashMap<String,Object>();
taskInfo.put("type", MyPluginTask.class);
taskInfo.put("description", "Generates blah from blah.");
taskInfo.put("group", "Blah");
Task myPluginTask = project.task(taskInfo, "myPluginTask");
// Define conventions and attach them to tasks
MyPluginExtensions extensions = new MyPluginExtensions(project);
myPluginTask.getExtensions().add(
"sourceDir",
extensions.sourceDir);
myPluginTask.getExtensions().add(
"outputDir",
extensions.outputDir);
}
}
This approach, however, doesn't seem to be correct. A new project property is shows up in the project.ext namespace. I expect to be able to address the plugin extensions as:
in my build.gradle:
myPluginTask.sourceDir = file('the/main/src')
myPluginTask.outputDir = file('the/output')
However, when I put such things in a gradle script that uses my plugin and try to set this property, gradle tells me I can't set it:
* What went wrong:
A problem occurred evaluating script.
> There's an extension registered with name 'sourceDir'. You should not reassign it via a property setter.
So, what's the right way to add property extensions for a task in a Java-based Gradle plugin?
EDIT:
Based on some other SO posts, I tried just adding my extensions object in one shot:
// first attempt:
//myPluginTask.getExtensions().add("sourceDir", extensions.sourceDir);
//myPluginTask.getExtensions().add("outputDir",extensions.outputDir);
// second attempt
myPluginTask.getExtensions().add("myPluginTask", extensions);
This appears to work. However, Gradle is now complaining that I've added a dynamic property:
Deprecated dynamic property: "sourceDir" on "task ':myPluginTask'", value: "/Users/jfer...".
So, again, what's the right way to add a plugin extension property?
EDIT 2
So, taking yet another shot at this, I'm adding the extension to the project object and using the create method instead:
// first attempt:
//myPluginTask.getExtensions().add("sourceDir", extensions.sourceDir);
//myPluginTask.getExtensions().add("outputDir",extensions.outputDir);
// second attempt
// myPluginTask.getExtensions().add("myPluginTask", extensions);
// third attempt
project.getExtensions().create("myPluginTask", MyPluginExtensions.class, project);
However, this fails for a couple of reasons:
Creating a properties extension with the same name ("myPluginTask") as the task creates a collision between the task name and extension name, causing the task to disappear from gradle's perspective (and throw oblique errors, such as "No such property: dependsOn for class ...MyPluginExtensions").
If I provide a name that does not collide with a task name (e.g., "myPluginPropExt"), the create() method works, but DOES NOT add the extension in its own namespace as expected (e.g., project.myPluginPropExt.propertyName and instead adds it in the project namespace (e.g., project.propertyName) which is not correct and causes Gradle to throw a bunch of "deprecated dynamic property" warnings.
So here is a solution to my problem:
public class MyPlugin implements Plugin<Project> {
#Override
public void apply(Project project) {
Map<String,Object> taskInfo = new HashMap<String,Object>();
taskInfo.put("type", MyPluginTask.class);
taskInfo.put("description", "Generates blah from blah.");
taskInfo.put("group", "Blah");
Task myPluginTask = project.task(taskInfo, "myPluginTask");
// Define conventions and attach them to tasks
MyPluginExtensions extensions = new MyPluginExtensions(project);
// the magic extension code:
project.getExtensions().add("myPluginName", extensions);
}
}
Now I can set a value for one of the extension properties in my gradle.build file like so (and I don't get a warning about adding deprecated dynamic properties):
myPluginName.sourceDir = file('the/main/src')
The final trick is to get this value in my Plugin's task:
public class MyPluginTask extends DefaultTask {
#TaskAction
public void action() {
MyPluginExtensions extensions = (MyPluginExtensions) getProject()
.getExtensions().findByName("myPluginName");
System.out.println("sourceDir value: " + extensions.sourceDir);
}
}
This works, but what annoys me about this solution is that I want to be able to put the extension properties in the same namespace as the task (e.g., myPluginTask.sourceDir) which I have seen in groovy-based plugins, but this apparently is not supported or just doesn't work.
In the meantime, hope this helps someone else.
The code is adding an extension to a task (rather than a project), which is rarely useful. After that, it tries to set myPluginTask.sourceDir = file('the/main/src'), which isn't possible because the extension was just registered under that same name.
When your task and your extension have the same name, you can do this in your build.gradle:
myPluginTask.sourceDir = file('the/main/src')
project.tasks.myPluginTask.dependsOn clean

Categories