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

Gradle: Classpath elements not on classpath during plugin execution #1726

Open
LarsBodewig opened this issue Nov 10, 2024 · 16 comments
Open
Assignees
Labels
Milestone

Comments

@LarsBodewig
Copy link
Contributor

LarsBodewig commented Nov 10, 2024

Multiple byte-buddy gradle tasks annotate an input property as classpath but the input is not actually used as the classpath for execution.

I tried to load some classes in my custom plugin, expecting to find them on the classpath, however they are never available, unless they are also part of an origin or used for discovery.

I understand that byte-buddy does not actually need any other classpath elements that are not part of an origin, so I am not sure if it would be smart to load more, however the use of the annotation makes this a bit misleading.

Would it be possible to load all classes that are configured as classpath? Or idk run the plugins in a custom classloader that queries the ClassFileLocator?

@raphw
Copy link
Owner

raphw commented Nov 11, 2024

You mean this configuration? There are multiple ways to configure the Gradle plugin. You can specify a name and then the plugin will be resolved from within the plugin's class path, or you can configure the plugin and its dependencies directly. How did you approach this?

@raphw raphw self-assigned this Nov 11, 2024
@raphw raphw added the question label Nov 11, 2024
@raphw raphw added this to the 1.15.10 milestone Nov 11, 2024
@LarsBodewig
Copy link
Contributor Author

LarsBodewig commented Nov 11, 2024

Judging from the name, I expected the classpath property to allow arbitrary classes to be supplied and accessed in the plugin.

buildscript {
    dependencies {
        classpath 'net.bytebuddy:byte-buddy-gradle-plugin:1.15.0'
        classpath 'net.bytebuddy:byte-buddy:1.15.0'
    }
}

configurations {
    myCustomClasspath
}

dependencies {
    myCustomClasspath 'my:special:jar' // containing my.special.jar.MySpecialClass
}

import net.bytebuddy.build.Plugin
import net.bytebuddy.build.gradle.ByteBuddyJarTask
import net.bytebuddy.description.type.TypeDescription
import net.bytebuddy.dynamic.ClassFileLocator
import net.bytebuddy.dynamic.DynamicType

class SimplePlugin implements Plugin {
    @Override
    void close() throws IOException {
    }

    @Override
    DynamicType.Builder<?> apply(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassFileLocator classFileLocator) {
        Class.forName("my.special.jar.MySpecialClass") // fails since the myCustomClasspath configuration is not resolved/available
        return builder
    }

    @Override
    boolean matches(TypeDescription typeDefinitions) {
        return true
    }
}

tasks.register("myByteBuddyTask", ByteBuddyJarsTask) {
    source = files("libs")
    target = file("transformedLibs")
    classPath = project.getConfigurations().getByName("myCustomClasspath")
    transformation {
        plugin = SimplePlugin
    }
}

The Class.forName() is just a simple example. I actually want to detect classes from 'my:special:jar' that have a special annotation.

Right now I have to include 'my:special:jar' in the buildscript classpath to be available in the SimplePlugin.

buildscript {
    dependencies {
        classpath 'net.bytebuddy:byte-buddy-gradle-plugin:1.15.0'
        classpath 'net.bytebuddy:byte-buddy:1.15.0'
        classpath 'my:special:jar'
    }
}

However I try to avoid that since those classes are not needed outside of the custom byte-buddy plugin.

As far as I understand, that is exactly the purpose of the classpath property in gradle tasks like JavaExec - using classes during task runtime but not script compilation. Is there a better way to access arbitrary classes from my plugin?

@LarsBodewig
Copy link
Contributor Author

I tried creating a custom classloader that reads the byte[] from the class file locator. But I always get java.lang.ClassFormatError: Truncated class file.

Does the class file locator do anything special with the class files or are my classes the issue?

@raphw
Copy link
Owner

raphw commented Nov 25, 2024

You can also configure dependencies in the transformation block. I think this is where your additional dependencies need to go. The class path property is mostly used for reading.

@LarsBodewig
Copy link
Contributor Author

How? I don't see a property in the Transformation class: net.bytebuddy.build.gradle.Transformation

@raphw
Copy link
Owner

raphw commented Dec 1, 2024

This would be done via the extension in the discovery set: https://github.com/raphw/byte-buddy/blob/master/byte-buddy-gradle-plugin/src/main/java/net/bytebuddy/build/gradle/ByteBuddyTaskExtension.java#L45

The class path is meant for plugins that want to discover transformers automatically via a service loader file.

@LarsBodewig
Copy link
Contributor Author

I set both discoverySet and classpath to my gradle configuration:

task.setDiscoverySet(myConfiguration);
task.setClassPath(myConfiguration);
task.doFirst(t -> myConfiguration.getFiles().forEach(System.out::println));

The System.out shows all jar files as expected. However if I use a tool like ClassGraph to scan the classpath from my custom byte-buddy plugin, I only see all the gradle jars and the jar with my plugin class. I still am unable to see the jars from myConfiguration.

I also tried again with the ClassLoader implementation, but I always get the ClassFormatError with the byte[] from the ClassFileLocator.

@raphw
Copy link
Owner

raphw commented Dec 9, 2024

You can look up the class file, but get a ClassFormatError?

@LarsBodewig
Copy link
Contributor Author

I think my custom ClassLoader works fine after all. It does find classes inside the set classpath given the class name:

public class ClassFileLoader extends ClassLoader {

    private final ClassFileLocator locator;

    public ClassFileLoader(ClassFileLocator locator) {
        this.locator = locator;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            ClassFileLocator.Resolution resolution = locator.locate(name);
            if (resolution.isResolved()) {
                byte[] bytes = resolution.resolve();
                return this.defineClass(name, bytes, 0, bytes.length);
            }
            return super.findClass(name);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

However it is impossible to locate classes without explicitly providing the name e.g. by using scanners like ClassGraph, since - again - the source URIs of the ClassFileLocator are not part of the actual classpath. And there is no way to access the URLs the ClassFileLocator is reading from.

What about a new method in ClassFileLocator to expose the source URIs?

public interface ClassFileLocator extends Closeable {
  //...
  /** Returns the sources this ClassFileLocator tries to locate classes in */
  List<URI> getSources() throws IOException;
}

Or is there any way to access the classpath property of the gradle task extension (task.setDiscoverySet(myConfiguration);) from inside the custom plugin? For example with another interface in Plugin?

interface WithClasspath extends Plugin {
  /** Implement this in your custom plugin to get ahold of the classpath property */
  void processClasspathProperty(Iterable<File> classpath);
}

@raphw
Copy link
Owner

raphw commented Dec 10, 2024

I can add Project as an injectable variable to plugins within Gradle. This would allow you to navigate to the task and resolve it from there. Does the ClassFileLocator that is provided not allow you to access the resource?

@LarsBodewig
Copy link
Contributor Author

ClassFileLocator only provides a single method: https://github.com/raphw/byte-buddy/blob/master/byte-buddy-dep/src/main/java/net/bytebuddy/dynamic/ClassFileLocator.java#L77

The Project is gradle specific - I think it would be bad if custom plugins wouldn't work the same with both maven and gradle.
But both the maven mojo and the gradle task could add their classpath elements to the plugin engine and the engine could provide them to the custom plugin. I created a PR to showcase what I mean: #1734

@raphw
Copy link
Owner

raphw commented Dec 11, 2024

I do not think a new method is the right approach. The class path might not even be available, and if it is available, it should be provided through construction. I agree that injecting the project should be a last resort, but we could provide a File[] argument, too.

@raphw
Copy link
Owner

raphw commented Dec 11, 2024

I added a change to master. Would that work for your needs?

@LarsBodewig
Copy link
Contributor Author

So a custom plugin with a constructor parameter File[] would automatically get the classPath? Sounds good, I will try it out.

Is it still possible to use more parameters additionally to File[]?

@LarsBodewig
Copy link
Contributor Author

I am missing the gradle plugin marker in my local maven repository when I build and install locally, so I can't test the plugin with gradle right now. Am I missing something?

@raphw
Copy link
Owner

raphw commented Dec 12, 2024

Yes, a File[] would now represent the class path.

What do you mean by plugin marker?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants