From 72a31ddb879e9a0131f9241fc7d83f4cdda68a28 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Thu, 4 Dec 2025 15:53:35 -0500 Subject: [PATCH 1/3] Refactored InstrumentPlugin from Groovy to Kotlin. --- buildSrc/build.gradle.kts | 4 +- .../src/main/groovy/InstrumentPlugin.groovy | 210 -------------- .../main/groovy/InstrumentingPlugin.groovy | 112 -------- .../plugin/instrument/InstrumentPlugin.kt | 258 ++++++++++++++++++ .../plugin/instrument/InstrumentingPlugin.kt | 122 +++++++++ .../gradle/plugin/muzzle/MuzzlePlugin.kt | 2 +- 6 files changed, 382 insertions(+), 326 deletions(-) delete mode 100644 buildSrc/src/main/groovy/InstrumentPlugin.groovy delete mode 100644 buildSrc/src/main/groovy/InstrumentingPlugin.groovy create mode 100644 buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt create mode 100644 buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentingPlugin.kt diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index b0ec91d7fe2..f73cba9b654 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -1,5 +1,4 @@ plugins { - groovy `java-gradle-plugin` `kotlin-dsl` `jvm-test-suite` @@ -16,7 +15,7 @@ gradlePlugin { plugins { create("instrument-plugin") { id = "dd-trace-java.instrument" - implementationClass = "InstrumentPlugin" + implementationClass = "datadog.gradle.plugin.instrument.InstrumentPlugin" } create("muzzle-plugin") { id = "dd-trace-java.muzzle" @@ -55,7 +54,6 @@ repositories { dependencies { implementation(gradleApi()) - implementation(localGroovy()) implementation("net.bytebuddy", "byte-buddy-gradle-plugin", "1.18.1") diff --git a/buildSrc/src/main/groovy/InstrumentPlugin.groovy b/buildSrc/src/main/groovy/InstrumentPlugin.groovy deleted file mode 100644 index 781e059d765..00000000000 --- a/buildSrc/src/main/groovy/InstrumentPlugin.groovy +++ /dev/null @@ -1,210 +0,0 @@ -import org.gradle.api.DefaultTask -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.Directory -import org.gradle.api.file.DirectoryProperty -import org.gradle.api.invocation.BuildInvocationDetails -import org.gradle.api.provider.ListProperty -import org.gradle.api.provider.Property -import org.gradle.api.tasks.Classpath -import org.gradle.api.tasks.Input -import org.gradle.api.tasks.InputDirectory -import org.gradle.api.tasks.InputFiles -import org.gradle.api.tasks.Optional -import org.gradle.api.tasks.OutputDirectory -import org.gradle.api.tasks.TaskAction -import org.gradle.api.tasks.compile.AbstractCompile -import org.gradle.jvm.toolchain.JavaLanguageVersion -import org.gradle.jvm.toolchain.JavaToolchainService -import org.gradle.workers.WorkAction -import org.gradle.workers.WorkParameters -import org.gradle.workers.WorkerExecutor - -import javax.inject.Inject -import java.util.concurrent.ConcurrentHashMap -import java.util.regex.Matcher - -/** - * instrument task plugin which performs build-time instrumentation of classes. - */ -@SuppressWarnings('unused') -class InstrumentPlugin implements Plugin { - @Override - void apply(Project project) { - InstrumentExtension extension = project.extensions.create('instrument', InstrumentExtension) - - project.tasks.matching { - it.name in ['compileJava', 'compileScala', 'compileGroovy'] || - it.name =~ /compileMain_.+Java/ - }.all { - AbstractCompile compileTask = it as AbstractCompile - Matcher versionMatcher = it.name =~ /compileMain_(.+)Java/ - project.afterEvaluate { - if (!compileTask.source.empty) { - String sourceSetSuffix = null - String javaVersion = null - if (versionMatcher.matches()) { - sourceSetSuffix = versionMatcher.group(1) - if (sourceSetSuffix ==~ /java\d+/) { - javaVersion = sourceSetSuffix[4..-1] - } - } - - // insert intermediate 'raw' directory for unprocessed classes - Directory classesDir = compileTask.destinationDirectory.get() - Directory rawClassesDir = classesDir.dir( - "../raw${sourceSetSuffix ? "_$sourceSetSuffix" : ''}/") - compileTask.destinationDirectory.set(rawClassesDir.asFile) - - // insert task between compile and jar, and before test* - String instrumentTaskName = compileTask.name.replace('compile', 'instrument') - def instrumentTask = project.tasks.register(instrumentTaskName, InstrumentTask) { - // Task configuration - it.group = 'Byte Buddy' - it.description = "Instruments the classes compiled by ${compileTask.name}" - it.inputs.dir(compileTask.destinationDirectory) - it.outputs.dir(classesDir) - // Task inputs - it.javaVersion = javaVersion - def instrumenterConfiguration = project.configurations.named('instrumentPluginClasspath') - if (instrumenterConfiguration.present) { - it.pluginClassPath.from(instrumenterConfiguration.get()) - } - it.plugins = extension.plugins - it.instrumentingClassPath.from( - findCompileClassPath(project, it.name) + - rawClassesDir + - findAdditionalClassPath(extension, it.name) - ) - it.sourceDirectory = rawClassesDir - // Task output - it.targetDirectory = classesDir - } - if (javaVersion) { - project.tasks.named(project.sourceSets."main_java${javaVersion}".classesTaskName) { - it.dependsOn(instrumentTask) - } - } else { - project.tasks.named(project.sourceSets.main.classesTaskName) { - it.dependsOn(instrumentTask) - } - } - } - } - } - } - - static findCompileClassPath(Project project, String taskName) { - def matcher = taskName =~ /instrument([A-Z].+)Java/ - def cfgName = matcher.matches() ? "${matcher.group(1).uncapitalize()}CompileClasspath" : 'compileClasspath' - project.configurations.named(cfgName).findAll { - it.name != 'previous-compilation-data.bin' && !it.name.endsWith('.gz') - } - } - - static findAdditionalClassPath(InstrumentExtension extension, String taskName) { - extension.additionalClasspath.getOrDefault(taskName, []).collect { - // insert intermediate 'raw' directory for unprocessed classes - def fileName = it.get().asFile.name - it.get().dir("../${fileName.replaceFirst('^main', 'raw')}") - } - } -} - -abstract class InstrumentExtension { - abstract ListProperty getPlugins() - Map> additionalClasspath = [:] -} - -abstract class InstrumentTask extends DefaultTask { - @Input @Optional - String javaVersion - @InputFiles @Classpath - abstract ConfigurableFileCollection getPluginClassPath() - @Input - ListProperty plugins - @InputFiles @Classpath - abstract ConfigurableFileCollection getInstrumentingClassPath() - @InputDirectory - Directory sourceDirectory - - @OutputDirectory - Directory targetDirectory - - @Inject - abstract JavaToolchainService getJavaToolchainService() - @Inject - abstract BuildInvocationDetails getInvocationDetails() - @Inject - abstract WorkerExecutor getWorkerExecutor() - - @TaskAction - instrument() { - workQueue().submit(InstrumentAction.class, parameters -> { - parameters.buildStartedTime.set(this.invocationDetails.buildStartedTime) - parameters.pluginClassPath.from(this.pluginClassPath) - parameters.plugins.set(this.plugins) - parameters.instrumentingClassPath.setFrom(this.instrumentingClassPath) - parameters.sourceDirectory.set(this.sourceDirectory.asFile) - parameters.targetDirectory.set(this.targetDirectory.asFile) - }) - } - - private workQueue() { - if (!this.javaVersion) { - this.javaVersion = "8" - } - def javaLauncher = this.javaToolchainService.launcherFor { spec -> - spec.languageVersion.set(JavaLanguageVersion.of(this.javaVersion)) - }.get() - return this.workerExecutor.processIsolation { spec -> - spec.forkOptions { fork -> - fork.executable = javaLauncher.executablePath - } - } - } -} - -interface InstrumentWorkParameters extends WorkParameters { - Property getBuildStartedTime() - ConfigurableFileCollection getPluginClassPath() - ListProperty getPlugins() - ConfigurableFileCollection getInstrumentingClassPath() - DirectoryProperty getSourceDirectory() - DirectoryProperty getTargetDirectory() -} - -abstract class InstrumentAction implements WorkAction { - private static final Object lock = new Object() - private static final Map classLoaderCache = new ConcurrentHashMap<>() - private static volatile long lastBuildStamp - - @Override - void execute() { - String[] plugins = parameters.getPlugins().get() as String[] - String classLoaderKey = plugins.join(':') - - // reset shared class-loaders each time a new build starts - long buildStamp = parameters.buildStartedTime.get() - ClassLoader pluginCL = classLoaderCache.get(classLoaderKey) - if (lastBuildStamp < buildStamp || !pluginCL) { - synchronized (lock) { - pluginCL = classLoaderCache.get(classLoaderKey) - if (lastBuildStamp < buildStamp || !pluginCL) { - pluginCL = createClassLoader(parameters.pluginClassPath) - classLoaderCache.put(classLoaderKey, pluginCL) - lastBuildStamp = buildStamp - } - } - } - File sourceDirectory = parameters.getSourceDirectory().get().asFile - File targetDirectory = parameters.getTargetDirectory().get().asFile - ClassLoader instrumentingCL = createClassLoader(parameters.instrumentingClassPath, pluginCL) - InstrumentingPlugin.instrumentClasses(plugins, instrumentingCL, sourceDirectory, targetDirectory) - } - - static ClassLoader createClassLoader(cp, parent = InstrumentAction.classLoader) { - return new URLClassLoader(cp*.toURI()*.toURL() as URL[], parent as ClassLoader) - } -} diff --git a/buildSrc/src/main/groovy/InstrumentingPlugin.groovy b/buildSrc/src/main/groovy/InstrumentingPlugin.groovy deleted file mode 100644 index 7710ee12539..00000000000 --- a/buildSrc/src/main/groovy/InstrumentingPlugin.groovy +++ /dev/null @@ -1,112 +0,0 @@ -import net.bytebuddy.ClassFileVersion -import net.bytebuddy.build.EntryPoint -import net.bytebuddy.build.Plugin -import net.bytebuddy.description.type.TypeDescription -import net.bytebuddy.dynamic.ClassFileLocator -import net.bytebuddy.dynamic.scaffold.inline.MethodNameTransformer.Suffixing -import net.bytebuddy.utility.StreamDrainer -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -/** - * Performs build-time instrumentation of classes, called indirectly from InstrumentPlugin. - * (This is the byte-buddy side of the task; InstrumentPlugin contains the Gradle pieces.) - */ -class InstrumentingPlugin { - static final Logger log = LoggerFactory.getLogger(InstrumentingPlugin.class) - - static void instrumentClasses( - String[] plugins, ClassLoader instrumentingLoader, File sourceDirectory, File targetDirectory) - throws Exception { - - ClassLoader tccl = Thread.currentThread().getContextClassLoader() - try { - Thread.currentThread().setContextClassLoader(instrumentingLoader) - - List factories = new ArrayList<>() - for (String plugin : plugins) { - try { - Class pluginClass = (Class) instrumentingLoader.loadClass(plugin) - Plugin loadedPlugin = pluginClass.getConstructor(File.class).newInstance(targetDirectory) - factories.add(new Plugin.Factory.Simple(loadedPlugin)) - } catch (Throwable throwable) { - throw new IllegalStateException("Cannot resolve plugin: " + plugin, throwable) - } - } - - Plugin.Engine.Source source = new Plugin.Engine.Source.ForFolder(sourceDirectory) - Plugin.Engine.Target target = new Plugin.Engine.Target.ForFolder(targetDirectory) - - Plugin.Engine engine = - Plugin.Engine.Default.of( - EntryPoint.Default.REBASE, ClassFileVersion.ofThisVm(), Suffixing.withRandomSuffix()) - - Plugin.Engine.Summary summary = - engine - .with(Plugin.Engine.PoolStrategy.Default.FAST) - .with(new NonCachingClassFileLocator(instrumentingLoader)) - .with(new LoggingAdapter()) - .withErrorHandlers( - Plugin.Engine.ErrorHandler.Enforcing.ALL_TYPES_RESOLVED, - Plugin.Engine.ErrorHandler.Enforcing.NO_LIVE_INITIALIZERS, - new Plugin.Engine.ErrorHandler() { - @Delegate - Plugin.Engine.ErrorHandler delegate = Plugin.Engine.ErrorHandler.Failing.FAIL_LAST - - void onError(Map> throwables) { - throw new IllegalStateException("Failed to transform at least one type: " + throwables).tap { ise -> - throwables.values().flatten().each { - ise.addSuppressed(it) - } - }; - } - } - ) - .with(Plugin.Engine.Dispatcher.ForSerialTransformation.Factory.INSTANCE) - .apply(source, target, factories) - - if (!summary.getFailed().isEmpty()) { - throw new IllegalStateException("Failed to transform: " + summary.getFailed()) - } - } catch (Throwable e) { - Thread.currentThread().setContextClassLoader(tccl) - throw e - } - } - - static class LoggingAdapter extends Plugin.Engine.Listener.Adapter { - @Override - void onTransformation(TypeDescription typeDescription, List plugins) { - log.debug("Transformed {} using {}", typeDescription, plugins) - } - - @Override - void onError(TypeDescription typeDescription, Plugin plugin, Throwable throwable) { - log.warn("Failed to transform {} using {}", typeDescription, plugin, throwable) - } - } - - static class NonCachingClassFileLocator implements ClassFileLocator { - ClassLoader loader - - NonCachingClassFileLocator(ClassLoader loader) { - this.loader = loader - } - - @Override - Resolution locate(String name) throws IOException { - URL url = loader.getResource(name.replace('.', '/') + '.class') - if (null == url) { - return new Resolution.Illegal(name) - } - URLConnection uc = url.openConnection() - uc.setUseCaches(false) // avoid caching class-file resources in build-workers - try (InputStream is = uc.getInputStream()) { - return new Resolution.Explicit(StreamDrainer.DEFAULT.drain(is)) - } - } - - @Override - void close() {} - } -} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt new file mode 100644 index 00000000000..65c9e448f24 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt @@ -0,0 +1,258 @@ +package datadog.gradle.plugin.instrument + +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.Directory +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.invocation.BuildInvocationDetails +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.compile.AbstractCompile +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.jvm.toolchain.JavaToolchainService +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters +import org.gradle.workers.WorkQueue +import org.gradle.workers.WorkerExecutor +import java.net.URLClassLoader +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +/** + * instrument task plugin which performs build-time instrumentation of classes. + */ +@Suppress("unused") +class InstrumentPlugin : Plugin { + override fun apply(project: Project) { + val extension = project.extensions.create("instrument", InstrumentExtension::class.java) + + project.tasks.matching { + it.name in listOf("compileJava", "compileScala", "compileGroovy") || + it.name.matches(Regex("compileMain_.+Java")) + }.configureEach { + val compileTask = this as AbstractCompile + val versionRegex = Regex("compileMain_(.+)Java") + val versionMatch = versionRegex.matchEntire(compileTask.name) + + project.afterEvaluate { + if (!compileTask.source.isEmpty) { + var sourceSetSuffix: String? = null + var javaVer: String? = null + + if (versionMatch != null) { + sourceSetSuffix = versionMatch.groupValues[1] + if (sourceSetSuffix.matches(Regex("java\\d+"))) { + javaVer = sourceSetSuffix.substring(4) + } + } + + // insert intermediate 'raw' directory for unprocessed classes + val classesDir = compileTask.destinationDirectory.get() + val rawClassesDir = classesDir.dir( + "../raw${if (sourceSetSuffix != null) "_$sourceSetSuffix" else ""}/" + ) + compileTask.destinationDirectory.set(rawClassesDir.asFile) + + // insert task between compile and jar, and before test* + val instrumentTaskName = compileTask.name.replace("compile", "instrument") + val instrumentTask = project.tasks.register( + instrumentTaskName, + InstrumentTask::class.java + ) { + // Task configuration + group = "Byte Buddy" + description = "Instruments the classes compiled by ${compileTask.name}" + inputs.dir(compileTask.destinationDirectory) + outputs.dir(classesDir) + + // Task inputs + javaVersion = javaVer + val instrumenterConfiguration = project.configurations.named("instrumentPluginClasspath") + if (instrumenterConfiguration.isPresent) { + pluginClassPath.from(instrumenterConfiguration.get()) + } + + plugins.set(extension.plugins) + instrumentingClassPath.from( + findCompileClassPath(project, name) + + rawClassesDir + + findAdditionalClassPath(extension, name) + ) + sourceDirectory.set(rawClassesDir) + + // Task output + targetDirectory.set(classesDir) + } + + if (javaVer != null) { + project.tasks.named( + project + .extensions + .getByName("sourceSets") + .let { it as org.gradle.api.tasks.SourceSetContainer } + .getByName("main_java${javaVer}").classesTaskName) { + dependsOn(instrumentTask) + } + } else { + project.tasks.named( + project + .extensions + .getByName("sourceSets") + .let { it as org.gradle.api.tasks.SourceSetContainer } + .getByName("main").classesTaskName) { + dependsOn(instrumentTask) + } + } + } + } + } + } + + companion object { + fun findCompileClassPath(project: Project, taskName: String): FileCollection { + val matcher = Regex("instrument([A-Z].+)Java").matchEntire(taskName) + val cfgName = if (matcher != null) { + "${matcher.groupValues[1].replaceFirstChar { it.lowercase() }}CompileClasspath" + } else { + "compileClasspath" + } + + return project.configurations.named(cfgName).get().filter { + it.name != "previous-compilation-data.bin" && !it.name.endsWith(".gz") + } + } + + fun findAdditionalClassPath(extension: InstrumentExtension, taskName: String): List { + val entries = extension.additionalClasspath[taskName].orEmpty() + + return entries.map { dirProp -> + // insert intermediate 'raw' directory for unprocessed classes + val dir = dirProp.get() + val fileName = dir.asFile.name + dir.dir("../${fileName.replaceFirst(Regex("^main"), "raw")}") + } + } + } +} + +abstract class InstrumentExtension { + abstract val plugins: ListProperty + val additionalClasspath: MutableMap> = mutableMapOf() +} + +abstract class InstrumentTask @Inject constructor() : DefaultTask() { + @get:Input + @get:Optional + var javaVersion: String? = null + + @get:InputFiles + @get:Classpath + abstract val pluginClassPath: ConfigurableFileCollection + + @get:Input + abstract val plugins: ListProperty + + @get:InputFiles + @get:Classpath + abstract val instrumentingClassPath: ConfigurableFileCollection + + @get:InputDirectory + abstract val sourceDirectory: DirectoryProperty + + @get:OutputDirectory + abstract val targetDirectory: DirectoryProperty + + @get:Inject + abstract val javaToolchainService: JavaToolchainService + + @get:Inject + abstract val invocationDetails: BuildInvocationDetails + + @get:Inject + abstract val workerExecutor: WorkerExecutor + + @TaskAction + fun instrument() { + workQueue().submit(InstrumentAction::class.java) { + buildStartedTime.set(invocationDetails.buildStartedTime) + pluginClassPath.from(pluginClassPath) + plugins.set(plugins) + instrumentingClassPath.from(instrumentingClassPath) + sourceDirectory.set(sourceDirectory) + targetDirectory.set(targetDirectory) + } + } + + private fun workQueue(): WorkQueue { + val effectiveJavaVersion = javaVersion ?: "8" + val javaLauncher = javaToolchainService.launcherFor { + languageVersion.set(JavaLanguageVersion.of(effectiveJavaVersion)) + }.get() + + return workerExecutor.processIsolation { + forkOptions { + executable = javaLauncher.executablePath.asFile.absolutePath + } + } + } +} + +interface InstrumentWorkParameters : WorkParameters { + val buildStartedTime: Property + val pluginClassPath: ConfigurableFileCollection + val plugins: ListProperty + val instrumentingClassPath: ConfigurableFileCollection + val sourceDirectory: DirectoryProperty + val targetDirectory: DirectoryProperty +} + +abstract class InstrumentAction : WorkAction { + companion object { + private val lock = Any() + private val classLoaderCache: MutableMap = ConcurrentHashMap() + + @Volatile + private var lastBuildStamp: Long = 0 + + fun createClassLoader( + cp: ConfigurableFileCollection, + parent: ClassLoader = InstrumentAction::class.java.classLoader + ): ClassLoader { + val urls = cp.files.map { it.toURI().toURL() }.toTypedArray() + return URLClassLoader(urls, parent) + } + } + + override fun execute() { + val plugins = parameters.plugins.get().toTypedArray() + val classLoaderKey = plugins.joinToString(":") + + // reset shared class-loaders each time a new build starts + val buildStamp = parameters.buildStartedTime.get() + var pluginCL: ClassLoader? = classLoaderCache[classLoaderKey] + if (lastBuildStamp < buildStamp || pluginCL == null) { + synchronized(lock) { + pluginCL = classLoaderCache[classLoaderKey] + if (lastBuildStamp < buildStamp || pluginCL == null) { + pluginCL = createClassLoader(parameters.pluginClassPath) + classLoaderCache[classLoaderKey] = pluginCL + lastBuildStamp = buildStamp + } + } + } + val sourceDirectory = parameters.sourceDirectory.get().asFile + val targetDirectory = parameters.targetDirectory.get().asFile + val instrumentingCL = createClassLoader(parameters.instrumentingClassPath, pluginCL!!) + InstrumentingPlugin.instrumentClasses(plugins, instrumentingCL, sourceDirectory, targetDirectory) + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentingPlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentingPlugin.kt new file mode 100644 index 00000000000..81b459f4132 --- /dev/null +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentingPlugin.kt @@ -0,0 +1,122 @@ +package datadog.gradle.plugin.instrument + +import net.bytebuddy.ClassFileVersion +import net.bytebuddy.build.EntryPoint +import net.bytebuddy.build.Plugin +import net.bytebuddy.description.type.TypeDescription +import net.bytebuddy.dynamic.ClassFileLocator +import net.bytebuddy.dynamic.scaffold.inline.MethodNameTransformer.Suffixing +import net.bytebuddy.utility.StreamDrainer +import org.slf4j.LoggerFactory +import java.io.File +import java.io.IOException +import java.io.InputStream + +/** + * Performs build-time instrumentation of classes, called indirectly from InstrumentPlugin. + * (This is the byte-buddy side of the task; InstrumentPlugin contains the Gradle pieces.) + */ +class InstrumentingPlugin { + companion object { + private val log = LoggerFactory.getLogger(InstrumentingPlugin::class.java) + + @Throws(Exception::class) + fun instrumentClasses( + plugins: Array, + instrumentingLoader: ClassLoader, + sourceDirectory: File, + targetDirectory: File + ) { + val tccl = Thread.currentThread().contextClassLoader + try { + Thread.currentThread().contextClassLoader = instrumentingLoader + + val factories = plugins.map { plugin -> + try { + @Suppress("UNCHECKED_CAST") + val pluginClass = instrumentingLoader.loadClass(plugin) as Class + val loadedPlugin = pluginClass.getConstructor(File::class.java).newInstance(targetDirectory) + Plugin.Factory.Simple(loadedPlugin) + } catch (t: Throwable) { + throw IllegalStateException("Cannot resolve plugin: $plugin", t) + } + } + + val source = Plugin.Engine.Source.ForFolder(sourceDirectory) + val target = Plugin.Engine.Target.ForFolder(targetDirectory) + + val engine = Plugin.Engine.Default.of( + EntryPoint.Default.REBASE, ClassFileVersion.ofThisVm(), Suffixing.withRandomSuffix() + ) + + val summary = engine + .with(Plugin.Engine.PoolStrategy.Default.FAST) + .with(NonCachingClassFileLocator(instrumentingLoader)) + .with(LoggingAdapter()) + .withErrorHandlers( + Plugin.Engine.ErrorHandler.Enforcing.ALL_TYPES_RESOLVED, + Plugin.Engine.ErrorHandler.Enforcing.NO_LIVE_INITIALIZERS, + DelegatingErrorHandler(Plugin.Engine.ErrorHandler.Failing.FAIL_LAST) + ) + .with(Plugin.Engine.Dispatcher.ForSerialTransformation.Factory.INSTANCE) + .apply(source, target, factories) + + if (summary.failed.isNotEmpty()) { + throw IllegalStateException("Failed to transform: ${summary.failed}") + } + } catch (e: Throwable) { + Thread.currentThread().contextClassLoader = tccl + throw e + } + } + } + + private class DelegatingErrorHandler( + private val delegate: Plugin.Engine.ErrorHandler + ) : Plugin.Engine.ErrorHandler by delegate { + override fun onError(throwables: Map>) { + val ise = IllegalStateException( + "Failed to transform at least one type: $throwables" + ) + throwables.values + .flatten() + .forEach { ise.addSuppressed(it) } + + throw ise + } + } + + private class LoggingAdapter : Plugin.Engine.Listener.Adapter() { + override fun onTransformation(typeDescription: TypeDescription, plugins: List) { + log.debug("Transformed {} using {}", typeDescription, plugins) + } + + override fun onError(typeDescription: TypeDescription, plugin: Plugin, throwable: Throwable) { + log.warn("Failed to transform {} using {}", typeDescription, plugin, throwable) + } + } + + private class NonCachingClassFileLocator( + private val loader: ClassLoader + ) : ClassFileLocator { + @Throws(IOException::class) + override fun locate(name: String): ClassFileLocator.Resolution { + val url = loader.getResource(name.replace('.', '/') + ".class") + if (url == null) { + return ClassFileLocator.Resolution.Illegal(name) + } + + val uc = url.openConnection().apply { + useCaches = false // avoid caching class-file resources in build-workers + } + + uc.getInputStream().use { input: InputStream -> + return ClassFileLocator.Resolution.Explicit(StreamDrainer.DEFAULT.drain(input)) + } + } + + override fun close() { + // no-op + } + } +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt index b791f1f9a45..b62bcdb1e12 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt @@ -73,7 +73,7 @@ class MuzzlePlugin : Plugin { // Not adding group and description to keep this task from showing in `gradle tasks`. @Suppress("UNCHECKED_CAST") val compileMuzzle = project.tasks.register("compileMuzzle") { - dependsOn(project.tasks.withType(Class.forName("InstrumentTask") as Class)) // kotlin can't see groovy code + dependsOn(project.tasks.withType(Class.forName("datadog.gradle.plugin.instrument.InstrumentTask") as Class)) // kotlin can't see groovy code dependsOn(bootstrapProject.tasks.named("compileJava")) dependsOn(bootstrapProject.tasks.named("compileMain_java11Java")) dependsOn(toolingProject.tasks.named("compileJava")) From 68084c39682dd1e789fc957bdf37d656f67a6497 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Thu, 4 Dec 2025 16:03:52 -0500 Subject: [PATCH 2/3] Minor --- .../kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt index 65c9e448f24..fbccc953b9f 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt @@ -245,7 +245,7 @@ abstract class InstrumentAction : WorkAction { pluginCL = classLoaderCache[classLoaderKey] if (lastBuildStamp < buildStamp || pluginCL == null) { pluginCL = createClassLoader(parameters.pluginClassPath) - classLoaderCache[classLoaderKey] = pluginCL + classLoaderCache[classLoaderKey] = pluginCL!! lastBuildStamp = buildStamp } } From 19f38e443d45645af8542b8b3c51e7f9989b7d62 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Fri, 5 Dec 2025 13:36:40 -0500 Subject: [PATCH 3/3] Changed to follow previous Groovy implementation with .all{...} method --- .../datadog/gradle/plugin/instrument/InstrumentPlugin.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt index fbccc953b9f..ad72d7eab9d 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/instrument/InstrumentPlugin.kt @@ -3,6 +3,7 @@ package datadog.gradle.plugin.instrument import org.gradle.api.DefaultTask import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.Task import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.Directory import org.gradle.api.file.DirectoryProperty @@ -17,6 +18,7 @@ import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.TaskCollection import org.gradle.api.tasks.compile.AbstractCompile import org.gradle.jvm.toolchain.JavaLanguageVersion import org.gradle.jvm.toolchain.JavaToolchainService @@ -36,10 +38,13 @@ class InstrumentPlugin : Plugin { override fun apply(project: Project) { val extension = project.extensions.create("instrument", InstrumentExtension::class.java) - project.tasks.matching { + val tasks: TaskCollection = project.tasks.matching { it.name in listOf("compileJava", "compileScala", "compileGroovy") || it.name.matches(Regex("compileMain_.+Java")) - }.configureEach { + } + + // `all` can not be chained, as Kotlin choose to call method for collection, not for `TaskCollection`. + tasks.all { val compileTask = this as AbstractCompile val versionRegex = Regex("compileMain_(.+)Java") val versionMatch = versionRegex.matchEntire(compileTask.name)