Migrate codegen to be a plugin (#3401)
This commit is contained in:
parent
1f3faca6b9
commit
220ea414e3
|
@ -1,139 +0,0 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.gradle.bytebuddy;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import net.bytebuddy.build.gradle.ByteBuddySimpleTask;
|
||||
import net.bytebuddy.build.gradle.Transformation;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
import org.gradle.api.tasks.compile.AbstractCompile;
|
||||
|
||||
/**
|
||||
* Starting from version 1.10.15, ByteBuddy gradle plugin transformation task autoconfiguration is
|
||||
* hardcoded to be applied to javaCompile task. This causes the dependencies to be resolved during
|
||||
* an afterEvaluate that runs before any afterEvaluate specified in the build script, which in turn
|
||||
* makes it impossible to add dependencies in afterEvaluate. Additionally the autoconfiguration will
|
||||
* attempt to scan the entire project for tasks which depend on the compile task, to make each task
|
||||
* that depends on compile also depend on the transformation task. This is an extremely inefficient
|
||||
* operation in this project to the point of causing a stack overflow in some environments.
|
||||
*
|
||||
* <p>To avoid all the issues with autoconfiguration, this class manually configures the ByteBuddy
|
||||
* transformation task. This also allows it to be applied to source languages other than Java. The
|
||||
* transformation task is configured to run between the compile and the classes tasks, assuming no
|
||||
* other task depends directly on the compile task, but instead other tasks depend on classes task.
|
||||
* Contrary to how the ByteBuddy plugin worked in versions up to 1.10.14, this changes the compile
|
||||
* task output directory, as starting from 1.10.15, the plugin does not allow the source and target
|
||||
* directories to be the same. The transformation task then writes to the original output directory
|
||||
* of the compile task.
|
||||
*/
|
||||
public class ByteBuddyPluginConfigurator {
|
||||
private static final List<String> LANGUAGES = Arrays.asList("java", "scala", "kotlin");
|
||||
|
||||
private final Project project;
|
||||
private final SourceSet sourceSet;
|
||||
private final String pluginClassName;
|
||||
private final FileCollection inputClasspath;
|
||||
|
||||
public ByteBuddyPluginConfigurator(
|
||||
Project project, SourceSet sourceSet, String pluginClassName, FileCollection inputClasspath) {
|
||||
this.project = project;
|
||||
this.sourceSet = sourceSet;
|
||||
this.pluginClassName = pluginClassName;
|
||||
|
||||
// add build resources dir to classpath if it's present
|
||||
File resourcesDir = sourceSet.getOutput().getResourcesDir();
|
||||
this.inputClasspath =
|
||||
resourcesDir == null ? inputClasspath : inputClasspath.plus(project.files(resourcesDir));
|
||||
}
|
||||
|
||||
public void configure() {
|
||||
String taskName = getTaskName();
|
||||
|
||||
List<TaskProvider<?>> languageTasks =
|
||||
LANGUAGES.stream()
|
||||
.map(
|
||||
language -> {
|
||||
if (project.fileTree("src/" + sourceSet.getName() + "/" + language).isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String compileTaskName = sourceSet.getCompileTaskName(language);
|
||||
if (!project.getTasks().getNames().contains(compileTaskName)) {
|
||||
return null;
|
||||
}
|
||||
TaskProvider<?> compileTask = project.getTasks().named(compileTaskName);
|
||||
|
||||
// We also process resources for SPI classes.
|
||||
return createLanguageTask(
|
||||
compileTask, taskName + language, sourceSet.getProcessResourcesTaskName());
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
TaskProvider<?> byteBuddyTask =
|
||||
project.getTasks().register(taskName, task -> task.dependsOn(languageTasks));
|
||||
|
||||
project
|
||||
.getTasks()
|
||||
.named(sourceSet.getClassesTaskName())
|
||||
.configure(task -> task.dependsOn(byteBuddyTask));
|
||||
}
|
||||
|
||||
private TaskProvider<?> createLanguageTask(
|
||||
TaskProvider<?> compileTaskProvider, String name, String processResourcesTaskName) {
|
||||
return project
|
||||
.getTasks()
|
||||
.register(
|
||||
name,
|
||||
ByteBuddySimpleTask.class,
|
||||
task -> {
|
||||
task.setGroup("Byte Buddy");
|
||||
task.getOutputs().cacheIf(unused -> true);
|
||||
|
||||
Task maybeCompileTask = compileTaskProvider.get();
|
||||
if (maybeCompileTask instanceof AbstractCompile) {
|
||||
AbstractCompile compileTask = (AbstractCompile) maybeCompileTask;
|
||||
File classesDirectory = compileTask.getDestinationDirectory().getAsFile().get();
|
||||
File rawClassesDirectory =
|
||||
new File(classesDirectory.getParent(), classesDirectory.getName() + "raw")
|
||||
.getAbsoluteFile();
|
||||
|
||||
task.dependsOn(compileTask);
|
||||
compileTask.getDestinationDirectory().set(rawClassesDirectory);
|
||||
|
||||
task.setSource(rawClassesDirectory);
|
||||
task.setTarget(classesDirectory);
|
||||
task.setClassPath(compileTask.getClasspath());
|
||||
|
||||
task.dependsOn(compileTask, processResourcesTaskName);
|
||||
}
|
||||
|
||||
task.getTransformations().add(createTransformation(inputClasspath, pluginClassName));
|
||||
});
|
||||
}
|
||||
|
||||
private String getTaskName() {
|
||||
if (SourceSet.MAIN_SOURCE_SET_NAME.equals(sourceSet.getName())) {
|
||||
return "byteBuddy";
|
||||
} else {
|
||||
return sourceSet.getName() + "ByteBuddy";
|
||||
}
|
||||
}
|
||||
|
||||
private static Transformation createTransformation(
|
||||
FileCollection classPath, String pluginClassName) {
|
||||
Transformation transformation = new ClasspathTransformation(classPath, pluginClassName);
|
||||
transformation.setPlugin(ClasspathByteBuddyPlugin.class);
|
||||
return transformation;
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.gradle.bytebuddy;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.build.Plugin;
|
||||
import net.bytebuddy.description.type.TypeDescription;
|
||||
import net.bytebuddy.dynamic.ClassFileLocator;
|
||||
import net.bytebuddy.dynamic.DynamicType;
|
||||
|
||||
/**
|
||||
* Starting from version 1.10.15, ByteBuddy gradle plugin transformations require that plugin
|
||||
* classes are given as class instances instead of a class name string. To be able to still use a
|
||||
* plugin implementation that is not a buildscript dependency, this reimplements the previous logic
|
||||
* by taking a delegate class name and class path as arguments and loading the plugin class from the
|
||||
* provided classloader when the plugin is instantiated.
|
||||
*/
|
||||
public class ClasspathByteBuddyPlugin implements Plugin {
|
||||
private final Plugin delegate;
|
||||
|
||||
/**
|
||||
* classPath and className argument resolvers are explicitly added by {@link
|
||||
* ClasspathTransformation}, sourceDirectory is automatically resolved as by default any {@link
|
||||
* File} argument is resolved to source directory.
|
||||
*/
|
||||
public ClasspathByteBuddyPlugin(
|
||||
Iterable<File> classPath, File sourceDirectory, String className) {
|
||||
this.delegate = pluginFromClassPath(classPath, sourceDirectory, className);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DynamicType.Builder<?> apply(
|
||||
DynamicType.Builder<?> builder,
|
||||
TypeDescription typeDescription,
|
||||
ClassFileLocator classFileLocator) {
|
||||
|
||||
return delegate.apply(builder, typeDescription, classFileLocator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(TypeDescription typeDefinitions) {
|
||||
return delegate.matches(typeDefinitions);
|
||||
}
|
||||
|
||||
private static Plugin pluginFromClassPath(
|
||||
Iterable<File> classPath, File sourceDirectory, String className) {
|
||||
try {
|
||||
ClassLoader classLoader = classLoaderFromClassPath(classPath, sourceDirectory);
|
||||
Class<?> clazz = Class.forName(className, false, classLoader);
|
||||
return (Plugin) clazz.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to create ByteBuddy plugin instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static ClassLoader classLoaderFromClassPath(
|
||||
Iterable<File> classPath, File sourceDirectory) {
|
||||
List<URL> urls = new ArrayList<>();
|
||||
urls.add(fileAsUrl(sourceDirectory));
|
||||
|
||||
for (File file : classPath) {
|
||||
urls.add(fileAsUrl(file));
|
||||
}
|
||||
|
||||
return new URLClassLoader(urls.toArray(new URL[0]), ByteBuddy.class.getClassLoader());
|
||||
}
|
||||
|
||||
private static URL fileAsUrl(File file) {
|
||||
try {
|
||||
return file.toURI().toURL();
|
||||
} catch (MalformedURLException e) {
|
||||
throw new IllegalStateException("Cannot resolve " + file + " as URL", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package io.opentelemetry.instrumentation.gradle.bytebuddy;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import net.bytebuddy.build.Plugin.Factory.UsingReflection.ArgumentResolver;
|
||||
import net.bytebuddy.build.gradle.Transformation;
|
||||
import org.gradle.api.tasks.Classpath;
|
||||
import org.gradle.api.tasks.Input;
|
||||
|
||||
/**
|
||||
* Special implementation of {@link Transformation} is required as classpath argument must be
|
||||
* exposed to Gradle via {@link Classpath} annotation, which cannot be done if it is returned by
|
||||
* {@link Transformation#getArguments()}.
|
||||
*/
|
||||
public class ClasspathTransformation extends Transformation {
|
||||
private final Iterable<File> classpath;
|
||||
private final String pluginClassName;
|
||||
|
||||
public ClasspathTransformation(Iterable<File> classpath, String pluginClassName) {
|
||||
this.classpath = classpath;
|
||||
this.pluginClassName = pluginClassName;
|
||||
}
|
||||
|
||||
@Classpath
|
||||
public Iterable<? extends File> getClasspath() {
|
||||
return classpath;
|
||||
}
|
||||
|
||||
@Input
|
||||
public String getPluginClassName() {
|
||||
return pluginClassName;
|
||||
}
|
||||
|
||||
protected List<ArgumentResolver> makeArgumentResolvers() {
|
||||
return Arrays.asList(
|
||||
new ArgumentResolver.ForIndex(0, classpath),
|
||||
new ArgumentResolver.ForIndex(2, pluginClassName));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package io.opentelemetry.instrumentation.gradle.codegen
|
||||
|
||||
import net.bytebuddy.ByteBuddy
|
||||
import net.bytebuddy.build.Plugin
|
||||
import net.bytebuddy.description.type.TypeDescription
|
||||
import net.bytebuddy.dynamic.ClassFileLocator
|
||||
import net.bytebuddy.dynamic.DynamicType
|
||||
import java.io.File
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
|
||||
/**
|
||||
* Starting from version 1.10.15, ByteBuddy gradle plugin transformations require that plugin
|
||||
* classes are given as class instances instead of a class name string. To be able to still use a
|
||||
* plugin implementation that is not a buildscript dependency, this reimplements the previous logic
|
||||
* by taking a delegate class name and class path as arguments and loading the plugin class from the
|
||||
* provided classloader when the plugin is instantiated.
|
||||
*/
|
||||
class ClasspathByteBuddyPlugin(
|
||||
classPath: Iterable<File>, sourceDirectory: File, className: String
|
||||
) : Plugin {
|
||||
private val delegate = pluginFromClassPath(classPath, sourceDirectory, className)
|
||||
|
||||
override fun apply(
|
||||
builder: DynamicType.Builder<*>,
|
||||
typeDescription: TypeDescription,
|
||||
classFileLocator: ClassFileLocator
|
||||
): DynamicType.Builder<*> {
|
||||
return delegate.apply(builder, typeDescription, classFileLocator)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
delegate.close()
|
||||
}
|
||||
|
||||
override fun matches(typeDefinitions: TypeDescription): Boolean {
|
||||
return delegate.matches(typeDefinitions)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun pluginFromClassPath(
|
||||
classPath: Iterable<File>, sourceDirectory: File, className: String
|
||||
): Plugin {
|
||||
val classLoader = classLoaderFromClassPath(classPath, sourceDirectory)
|
||||
try {
|
||||
val clazz = Class.forName(className, false, classLoader)
|
||||
return clazz.getDeclaredConstructor().newInstance() as Plugin
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("Failed to create ByteBuddy plugin instance", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun classLoaderFromClassPath(
|
||||
classPath: Iterable<File>, sourceDirectory: File
|
||||
): ClassLoader {
|
||||
val urls = mutableListOf<URL>()
|
||||
urls.add(fileAsUrl(sourceDirectory))
|
||||
for (file in classPath) {
|
||||
urls.add(fileAsUrl(file))
|
||||
}
|
||||
return URLClassLoader(urls.toTypedArray(), ByteBuddy::class.java.classLoader)
|
||||
}
|
||||
|
||||
private fun fileAsUrl(file: File): URL {
|
||||
return file.toURI().toURL()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright The OpenTelemetry Authors
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
package io.opentelemetry.instrumentation.gradle.codegen
|
||||
|
||||
import net.bytebuddy.build.Plugin.Factory.UsingReflection.ArgumentResolver
|
||||
import net.bytebuddy.build.gradle.Transformation
|
||||
import org.gradle.api.tasks.Classpath
|
||||
import org.gradle.api.tasks.Input
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Special implementation of [Transformation] is required as classpath argument must be
|
||||
* exposed to Gradle via [Classpath] annotation, which cannot be done if it is returned by
|
||||
* [Transformation.getArguments].
|
||||
*/
|
||||
class ClasspathTransformation(
|
||||
@get:Classpath val classpath: Iterable<File>,
|
||||
@get:Input val pluginClassName: String
|
||||
) : Transformation() {
|
||||
override fun makeArgumentResolvers(): List<ArgumentResolver> {
|
||||
return listOf(
|
||||
ArgumentResolver.ForIndex(0, classpath),
|
||||
ArgumentResolver.ForIndex(2, pluginClassName)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import io.opentelemetry.instrumentation.gradle.codegen.ClasspathByteBuddyPlugin
|
||||
import io.opentelemetry.instrumentation.gradle.codegen.ClasspathTransformation
|
||||
import net.bytebuddy.build.gradle.ByteBuddySimpleTask
|
||||
import net.bytebuddy.build.gradle.Transformation
|
||||
|
||||
plugins {
|
||||
`java-library`
|
||||
}
|
||||
|
||||
/**
|
||||
* Starting from version 1.10.15, ByteBuddy gradle plugin transformation task autoconfiguration is
|
||||
* hardcoded to be applied to javaCompile task. This causes the dependencies to be resolved during
|
||||
* an afterEvaluate that runs before any afterEvaluate specified in the build script, which in turn
|
||||
* makes it impossible to add dependencies in afterEvaluate. Additionally the autoconfiguration will
|
||||
* attempt to scan the entire project for tasks which depend on the compile task, to make each task
|
||||
* that depends on compile also depend on the transformation task. This is an extremely inefficient
|
||||
* operation in this project to the point of causing a stack overflow in some environments.
|
||||
*
|
||||
* <p>To avoid all the issues with autoconfiguration, this plugin manually configures the ByteBuddy
|
||||
* transformation task. This also allows it to be applied to source languages other than Java. The
|
||||
* transformation task is configured to run between the compile and the classes tasks, assuming no
|
||||
* other task depends directly on the compile task, but instead other tasks depend on classes task.
|
||||
* Contrary to how the ByteBuddy plugin worked in versions up to 1.10.14, this changes the compile
|
||||
* task output directory, as starting from 1.10.15, the plugin does not allow the source and target
|
||||
* directories to be the same. The transformation task then writes to the original output directory
|
||||
* of the compile task.
|
||||
*/
|
||||
|
||||
val LANGUAGES = listOf("java", "scala", "kotlin")
|
||||
val pluginName = "io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin"
|
||||
|
||||
val codegen by configurations.creating {
|
||||
isCanBeConsumed = false
|
||||
isCanBeResolved = true
|
||||
}
|
||||
|
||||
val sourceSet = sourceSets.main.get()
|
||||
val inputClasspath = (sourceSet.output.resourcesDir?.let { codegen.plus(project.files(it)) } ?: codegen)
|
||||
.plus(configurations.runtimeClasspath.get())
|
||||
|
||||
val languageTasks = LANGUAGES.map { language ->
|
||||
if (fileTree("src/${sourceSet.name}/${language}").isEmpty) {
|
||||
return@map null
|
||||
}
|
||||
val compileTaskName = sourceSet.getCompileTaskName(language)
|
||||
if (!tasks.names.contains(compileTaskName)) {
|
||||
return@map null
|
||||
}
|
||||
val compileTask = tasks.named(compileTaskName)
|
||||
createLanguageTask(compileTask, "byteBuddy${language}")
|
||||
}.filterNotNull()
|
||||
|
||||
tasks {
|
||||
val byteBuddy by registering {
|
||||
dependsOn(languageTasks)
|
||||
}
|
||||
|
||||
named(sourceSet.classesTaskName) {
|
||||
dependsOn(byteBuddy)
|
||||
}
|
||||
}
|
||||
|
||||
fun createLanguageTask(
|
||||
compileTaskProvider: TaskProvider<*>, name: String): TaskProvider<*>? {
|
||||
return tasks.register<ByteBuddySimpleTask>(name) {
|
||||
setGroup("Byte Buddy")
|
||||
outputs.cacheIf { true }
|
||||
val compileTask = compileTaskProvider.get()
|
||||
if (compileTask is AbstractCompile) {
|
||||
val classesDirectory = compileTask.destinationDirectory.asFile.get()
|
||||
val rawClassesDirectory: File = File(classesDirectory.parent, "${classesDirectory.name}raw")
|
||||
.absoluteFile
|
||||
dependsOn(compileTask)
|
||||
compileTask.destinationDirectory.set(rawClassesDirectory)
|
||||
source = rawClassesDirectory
|
||||
target = classesDirectory
|
||||
classPath = compileTask.classpath
|
||||
dependsOn(compileTask, sourceSet.processResourcesTaskName)
|
||||
}
|
||||
|
||||
transformations.add(createTransformation(inputClasspath, pluginName))
|
||||
}
|
||||
}
|
||||
|
||||
fun createTransformation(classPath: FileCollection, pluginClassName: String): Transformation {
|
||||
return ClasspathTransformation(classPath, pluginClassName).apply {
|
||||
plugin = ClasspathByteBuddyPlugin::class.java
|
||||
}
|
||||
}
|
|
@ -1,18 +1,13 @@
|
|||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
import io.opentelemetry.instrumentation.gradle.bytebuddy.ByteBuddyPluginConfigurator
|
||||
|
||||
plugins {
|
||||
id("net.bytebuddy.byte-buddy")
|
||||
|
||||
id("otel.instrumentation-conventions")
|
||||
id("otel.javaagent-codegen")
|
||||
id("otel.shadow-conventions")
|
||||
}
|
||||
|
||||
val toolingRuntime by configurations.creating {
|
||||
isCanBeConsumed = false
|
||||
isCanBeResolved = true
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Integration tests may need to define custom instrumentation modules so we include the standard
|
||||
// instrumentation infrastructure for testing too.
|
||||
|
@ -39,15 +34,10 @@ dependencies {
|
|||
|
||||
testImplementation("org.testcontainers:testcontainers")
|
||||
|
||||
toolingRuntime(project(path = ":javaagent-tooling", configuration = "instrumentationMuzzle"))
|
||||
toolingRuntime(project(path = ":javaagent-extension-api", configuration = "instrumentationMuzzle"))
|
||||
add("codegen", project(path = ":javaagent-tooling", configuration = "instrumentationMuzzle"))
|
||||
add("codegen", project(path = ":javaagent-extension-api", configuration = "instrumentationMuzzle"))
|
||||
}
|
||||
|
||||
val pluginName = "io.opentelemetry.javaagent.tooling.muzzle.collector.MuzzleCodeGenerationPlugin"
|
||||
ByteBuddyPluginConfigurator(project, sourceSets.main.get(), pluginName,
|
||||
toolingRuntime.plus(configurations.runtimeClasspath.get()))
|
||||
.configure()
|
||||
|
||||
val testInstrumentation by configurations.creating {
|
||||
isCanBeConsumed = false
|
||||
isCanBeResolved = true
|
||||
|
|
Loading…
Reference in New Issue