Migrate codegen to be a plugin (#3401)

This commit is contained in:
Anuraag Agrawal 2021-06-25 08:35:59 +09:00 committed by GitHub
parent 1f3faca6b9
commit 220ea414e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 192 additions and 287 deletions

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}

View File

@ -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()
}
}
}

View File

@ -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)
)
}
}

View File

@ -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
}
}

View File

@ -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