From 220ea414e389c803f1a471960de1900f4acf2391 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Fri, 25 Jun 2021 08:35:59 +0900 Subject: [PATCH] Migrate codegen to be a plugin (#3401) --- .../ByteBuddyPluginConfigurator.java | 139 ------------------ .../bytebuddy/ClasspathByteBuddyPlugin.java | 90 ------------ .../bytebuddy/ClasspathTransformation.java | 45 ------ .../codegen/ClasspathByteBuddyPlugin.kt | 72 +++++++++ .../gradle/codegen/ClasspathTransformation.kt | 28 ++++ .../kotlin/otel.javaagent-codegen.gradle.kts | 89 +++++++++++ .../kotlin/otel.javaagent-testing.gradle.kts | 16 +- 7 files changed, 192 insertions(+), 287 deletions(-) delete mode 100644 buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ByteBuddyPluginConfigurator.java delete mode 100644 buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathByteBuddyPlugin.java delete mode 100644 buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathTransformation.java create mode 100644 buildSrc/src/main/kotlin/io/opentelemetry/instrumentation/gradle/codegen/ClasspathByteBuddyPlugin.kt create mode 100644 buildSrc/src/main/kotlin/io/opentelemetry/instrumentation/gradle/codegen/ClasspathTransformation.kt create mode 100644 buildSrc/src/main/kotlin/otel.javaagent-codegen.gradle.kts diff --git a/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ByteBuddyPluginConfigurator.java b/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ByteBuddyPluginConfigurator.java deleted file mode 100644 index 209f09aa49cc..000000000000 --- a/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ByteBuddyPluginConfigurator.java +++ /dev/null @@ -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. - * - *

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 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> 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; - } -} diff --git a/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathByteBuddyPlugin.java b/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathByteBuddyPlugin.java deleted file mode 100644 index e211a59e81a9..000000000000 --- a/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathByteBuddyPlugin.java +++ /dev/null @@ -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 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 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 classPath, File sourceDirectory) { - List 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); - } - } -} diff --git a/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathTransformation.java b/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathTransformation.java deleted file mode 100644 index 6d8cd0c3e86a..000000000000 --- a/buildSrc/src/main/java/io/opentelemetry/instrumentation/gradle/bytebuddy/ClasspathTransformation.java +++ /dev/null @@ -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 classpath; - private final String pluginClassName; - - public ClasspathTransformation(Iterable classpath, String pluginClassName) { - this.classpath = classpath; - this.pluginClassName = pluginClassName; - } - - @Classpath - public Iterable getClasspath() { - return classpath; - } - - @Input - public String getPluginClassName() { - return pluginClassName; - } - - protected List makeArgumentResolvers() { - return Arrays.asList( - new ArgumentResolver.ForIndex(0, classpath), - new ArgumentResolver.ForIndex(2, pluginClassName)); - } -} diff --git a/buildSrc/src/main/kotlin/io/opentelemetry/instrumentation/gradle/codegen/ClasspathByteBuddyPlugin.kt b/buildSrc/src/main/kotlin/io/opentelemetry/instrumentation/gradle/codegen/ClasspathByteBuddyPlugin.kt new file mode 100644 index 000000000000..01f456cc6ca0 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/opentelemetry/instrumentation/gradle/codegen/ClasspathByteBuddyPlugin.kt @@ -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, 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, 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, sourceDirectory: File + ): ClassLoader { + val urls = mutableListOf() + 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() + } + } +} diff --git a/buildSrc/src/main/kotlin/io/opentelemetry/instrumentation/gradle/codegen/ClasspathTransformation.kt b/buildSrc/src/main/kotlin/io/opentelemetry/instrumentation/gradle/codegen/ClasspathTransformation.kt new file mode 100644 index 000000000000..e382cc9158f5 --- /dev/null +++ b/buildSrc/src/main/kotlin/io/opentelemetry/instrumentation/gradle/codegen/ClasspathTransformation.kt @@ -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, + @get:Input val pluginClassName: String +) : Transformation() { + override fun makeArgumentResolvers(): List { + return listOf( + ArgumentResolver.ForIndex(0, classpath), + ArgumentResolver.ForIndex(2, pluginClassName) + ) + } +} diff --git a/buildSrc/src/main/kotlin/otel.javaagent-codegen.gradle.kts b/buildSrc/src/main/kotlin/otel.javaagent-codegen.gradle.kts new file mode 100644 index 000000000000..85adc3ad8efd --- /dev/null +++ b/buildSrc/src/main/kotlin/otel.javaagent-codegen.gradle.kts @@ -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. + * + *

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(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 + } +} diff --git a/buildSrc/src/main/kotlin/otel.javaagent-testing.gradle.kts b/buildSrc/src/main/kotlin/otel.javaagent-testing.gradle.kts index 89b0d7f65de5..23da25f61cbd 100644 --- a/buildSrc/src/main/kotlin/otel.javaagent-testing.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.javaagent-testing.gradle.kts @@ -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