diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 92365591b13..de8a29fe418 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,8 +15,8 @@ java { gradlePlugin { plugins { create("instrument-plugin") { - id = "dd-trace-java.instrument" - implementationClass = "InstrumentPlugin" + id = "dd-trace-java.build-time-instrumentation" + implementationClass = "datadog.gradle.plugin.instrument.BuildTimeInstrumentationPlugin" } create("muzzle-plugin") { diff --git a/buildSrc/src/main/groovy/InstrumentPlugin.groovy b/buildSrc/src/main/groovy/InstrumentPlugin.groovy deleted file mode 100644 index ebfe47b3c93..00000000000 --- a/buildSrc/src/main/groovy/InstrumentPlugin.groovy +++ /dev/null @@ -1,209 +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/datadog/gradle/plugin/instrument/BuildTimeInstrumentationExtension.groovy b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/BuildTimeInstrumentationExtension.groovy new file mode 100644 index 00000000000..480ebda4833 --- /dev/null +++ b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/BuildTimeInstrumentationExtension.groovy @@ -0,0 +1,9 @@ +package datadog.gradle.plugin.instrument + +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty + +abstract class BuildTimeInstrumentationExtension { + abstract ListProperty getPlugins() + abstract ListProperty getAdditionalClasspath() +} diff --git a/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/BuildTimeInstrumentationPlugin.groovy b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/BuildTimeInstrumentationPlugin.groovy new file mode 100644 index 00000000000..fbd3451b137 --- /dev/null +++ b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/BuildTimeInstrumentationPlugin.groovy @@ -0,0 +1,160 @@ +package datadog.gradle.plugin.instrument + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging +import org.gradle.api.tasks.SourceSet +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.compile.AbstractCompile + +import java.util.regex.Matcher + +/** + * Gradle plugin that applies ByteBuddy plugins to perform build-time bytecode instrumentation. + * + *

This plugin appends a post-processing action to existing main compilation + * tasks. This plugin allows to apply one or more ByteBuddy {@link net.bytebuddy.build.Plugin} + * implementations. + * + *

Configuration:

+ * There are two main configuration points: + *
    + *
  • The {@code buildTimeInstrumentation} extension, which allows to specify: + *
      + *
    • The list of ByteBuddy plugin class names to apply
    • + *
    • Additional classpath entries required to load the plugins
    • + *
    + *
  • + *
  • The {@code buildTimeInstrumentationPlugin} configuration, which allows to specify + * dependencies containing the ByteBuddy plugin implementations
  • + *
+ *

Example configuration: + * + *

+ * buildTimeInstrumentation {
+ *   plugins = ['com.example.MyByteBuddyPlugin', 'com.example.AnotherPlugin']
+ *   additionalClasspath = [file('path/to/additional/classes')]
+ * }
+ *
+ * dependencies {
+ *   buildTimeInstrumentationPlugin 'com.example:my-bytebuddy-plugin:1.0.0'
+ *   buildTimeInstrumentationPlugin project(path: ':some:project', configuration: 'buildTimeInstrumentationPlugin')
+ * }
+ * 
+ * + *

Requirements for ByteBuddy plugins:

+ *
    + *
  • Must implement {@link net.bytebuddy.build.Plugin}
  • + *
  • Must have a constructor accepting a {@link File} parameter (the target directory)
  • + *
  • Plugin classes must be available on the {@code buildTimeInstrumentationPlugin} configuration
  • + *
+ * + * @see BuildTimeInstrumentationExtension + * @see InstrumentPostProcessingAction + * @see ByteBuddyInstrumenter + */ +@SuppressWarnings('unused') +class BuildTimeInstrumentationPlugin implements Plugin { + public static final String DEFAULT_JAVA_VERSION = 'default' + public static final String BUILD_TIME_INSTRUMENTATION_PLUGIN_CONFIGURATION = 'buildTimeInstrumentationPlugin' + private final Logger logger = Logging.getLogger(BuildTimeInstrumentationPlugin) + + @Override + void apply(Project project) { + BuildTimeInstrumentationExtension extension = project.extensions.create('buildTimeInstrumentation', BuildTimeInstrumentationExtension) + project.configurations.register(BUILD_TIME_INSTRUMENTATION_PLUGIN_CONFIGURATION) + + + ['java', 'kotlin', 'scala', 'groovy'].each { langPluginId -> + project.pluginManager.withPlugin(langPluginId) { + configurePostCompilationInstrumentation(langPluginId, project, extension) + } + } + } + + private void configurePostCompilationInstrumentation(String language, Project project, BuildTimeInstrumentationExtension extension) { + project.extensions.configure(SourceSetContainer) { SourceSetContainer sourceSets -> + // For any "main" source-set configure its compile task + sourceSets.configureEach { SourceSet sourceSet -> + def sourceSetName = sourceSet.name + logger.info("[BuildTimeInstrumentationPlugin] source-set: $sourceSetName, language: $language") + + if (!sourceSetName.startsWith(SourceSet.MAIN_SOURCE_SET_NAME)) { + logger.debug("[BuildTimeInstrumentationPlugin] Skipping non-main source set {} for language {}", sourceSetName, language) + return + } + + def compileTaskName = sourceSet.getCompileTaskName(language) + logger.info("[BuildTimeInstrumentationPlugin] compile task name: " + compileTaskName) + + // For each _main_ compile task, append an instrumenting post-processing step + // Examples of compile tasks: + // - compileJava, + // - compileMain_java17Java, + // - compileMain_jetty904Java, + // - compileMain_play25Java, + // - compileKotlin, + // - compileScala, + // - compileGroovy, + project.tasks.withType(AbstractCompile).matching { + it.name == compileTaskName && !it.source.isEmpty() + }.configureEach { + logger.info('[BuildTimeInstrumentationPlugin] Applying buildTimeInstrumentationPlugin configuration as compile task input') + it.inputs.files(project.configurations.named(BUILD_TIME_INSTRUMENTATION_PLUGIN_CONFIGURATION)) + + if (it.source.isEmpty()) { + logger.debug("[BuildTimeInstrumentationPlugin] Skipping $compileTaskName for source set $sourceSetName as it has no source files") + return + } + + // Compute optional Java version + Matcher versionMatcher = compileTaskName =~ /compileMain_(.+)Java/ + String sourceSetSuffix = null + String javaVersion = null + if (versionMatcher.matches()) { + sourceSetSuffix = versionMatcher.group(1) + if (sourceSetSuffix ==~ /java\d+/) { + javaVersion = sourceSetSuffix[4..-1] + } + } + javaVersion = javaVersion ?: DEFAULT_JAVA_VERSION // null not accepted + it.inputs.property("javaVersion", javaVersion) + + it.inputs.property("plugins", extension.plugins) + + it.inputs.files(extension.additionalClasspath) + + // Temporary location for raw (un-instrumented) classes + DirectoryProperty tmpUninstrumentedClasses = project.objects.directoryProperty().value( + project.layout.buildDirectory.dir("tmp/${it.name}-raw-classes") + ) + + // Class path to use for instrumentation post-processing + ConfigurableFileCollection instrumentingClassPath = project.objects.fileCollection() + instrumentingClassPath.setFrom( + it.classpath, + extension.additionalClasspath, + tmpUninstrumentedClasses + ) + + // This were the post processing happens, i.e. were the instrumentation is applied + it.doLast( + "instrumentClasses", + project.objects.newInstance( + InstrumentPostProcessingAction, + javaVersion, + extension.plugins, + instrumentingClassPath, + it.destinationDirectory, + tmpUninstrumentedClasses + ) + ) + logger.info("[BuildTimeInstrumentationPlugin] Configured post-compile instrumentation for $compileTaskName for source-set $sourceSetName") + } + } + } + } +} diff --git a/buildSrc/src/main/groovy/InstrumentingPlugin.groovy b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/ByteBuddyInstrumenter.groovy similarity index 93% rename from buildSrc/src/main/groovy/InstrumentingPlugin.groovy rename to buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/ByteBuddyInstrumenter.groovy index 9a766971487..22504e11191 100644 --- a/buildSrc/src/main/groovy/InstrumentingPlugin.groovy +++ b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/ByteBuddyInstrumenter.groovy @@ -1,3 +1,5 @@ +package datadog.gradle.plugin.instrument + import net.bytebuddy.ClassFileVersion import net.bytebuddy.build.EntryPoint import net.bytebuddy.build.Plugin @@ -9,11 +11,11 @@ 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.) + * Performs build-time instrumentation of classes, called indirectly from BuildTimeInstrumentationPlugin. + * (This is the byte-buddy side of the task; BuildTimeInstrumentationPlugin contains the Gradle pieces.) */ -class InstrumentingPlugin { - static final Logger log = LoggerFactory.getLogger(InstrumentingPlugin.class) +class ByteBuddyInstrumenter { + static final Logger log = LoggerFactory.getLogger(ByteBuddyInstrumenter.class) static void instrumentClasses( String[] plugins, ClassLoader instrumentingLoader, File sourceDirectory, File targetDirectory) diff --git a/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/InstrumentAction.groovy b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/InstrumentAction.groovy new file mode 100644 index 00000000000..49f3c946412 --- /dev/null +++ b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/InstrumentAction.groovy @@ -0,0 +1,58 @@ +package datadog.gradle.plugin.instrument + +import java.nio.file.Path +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.model.ObjectFactory +import org.gradle.workers.WorkAction + +abstract class InstrumentAction implements WorkAction { + private static final Object lock = new Object() + private static final Map classLoaderCache = new ConcurrentHashMap<>() + private static volatile long lastBuildStamp + + @Inject + public abstract FileSystemOperations getFileSystemOperations(); + + @Inject + public abstract ObjectFactory getObjects(); + + @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 + } + } + } + Path classesDirectory = parameters.compilerOutputDirectory.get().asFile.toPath() + Path tmpUninstrumentedDir = parameters.tmpDirectory.get().asFile.toPath() + + // Original classes will be replaced by post-processed ones + fileSystemOperations.sync { + from(classesDirectory) + into(tmpUninstrumentedDir) + } + fileSystemOperations.delete { + delete(objects.fileTree().from(classesDirectory)) + } + + ClassLoader instrumentingCL = createClassLoader(parameters.instrumentingClassPath, pluginCL) + ByteBuddyInstrumenter.instrumentClasses(plugins, instrumentingCL, tmpUninstrumentedDir.toFile(), classesDirectory.toFile()) + } + + 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/datadog/gradle/plugin/instrument/InstrumentPostProcessingAction.groovy b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/InstrumentPostProcessingAction.groovy new file mode 100644 index 00000000000..c967d13e1df --- /dev/null +++ b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/InstrumentPostProcessingAction.groovy @@ -0,0 +1,86 @@ +package datadog.gradle.plugin.instrument + +import javax.inject.Inject +import org.gradle.api.Action +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileCollection +import org.gradle.api.invocation.BuildInvocationDetails +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.compile.AbstractCompile +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.gradle.jvm.toolchain.JavaToolchainService +import org.gradle.workers.WorkerExecutor + +abstract class InstrumentPostProcessingAction implements Action { + private final Logger logger = Logging.getLogger(InstrumentPostProcessingAction) + + @Inject + abstract Project getProject() + + @Inject + abstract JavaToolchainService getJavaToolchainService() + + @Inject + abstract BuildInvocationDetails getInvocationDetails() + + @Inject + abstract WorkerExecutor getWorkerExecutor() + + // Those cannot be private other wise Groovy will fail at runtime with a missing property ex + final JavaLanguageVersion javaVersion + final ListProperty plugins + final FileCollection instrumentingClassPath + final DirectoryProperty compilerOutputDirectory + final DirectoryProperty tmpDirectory + + @Inject + InstrumentPostProcessingAction( + String javaVersion, + ListProperty plugins, + FileCollection instrumentingClassPath, + DirectoryProperty compilerOutputDirectory, + DirectoryProperty tmpDirectory + ) { + this.javaVersion = javaVersion == BuildTimeInstrumentationPlugin.DEFAULT_JAVA_VERSION ? JavaLanguageVersion.current() : JavaLanguageVersion.of(javaVersion) + this.plugins = plugins + this.instrumentingClassPath = instrumentingClassPath + this.compilerOutputDirectory = compilerOutputDirectory + this.tmpDirectory = tmpDirectory + } + + @Override + void execute(AbstractCompile task) { + logger.info( + "[InstrumentPostProcessingAction] About to instrument classes \n" + + " javaVersion=${javaVersion}, \n" + + " plugins=${plugins.get()}, \n" + + " instrumentingClassPath=${instrumentingClassPath.files}, \n" + + " rawClassesDirectory=${compilerOutputDirectory.get().asFile}" + ) + def postCompileAction = this + workQueue().submit(InstrumentAction.class, parameters -> { + parameters.buildStartedTime.set(invocationDetails.buildStartedTime) + parameters.pluginClassPath.from( + project.configurations.named(BuildTimeInstrumentationPlugin.BUILD_TIME_INSTRUMENTATION_PLUGIN_CONFIGURATION) + ) + parameters.plugins.set(postCompileAction.plugins) + parameters.instrumentingClassPath.setFrom(postCompileAction.instrumentingClassPath) + parameters.compilerOutputDirectory.set(postCompileAction.compilerOutputDirectory) + parameters.tmpDirectory.set(postCompileAction.tmpDirectory) + }) + } + + private workQueue() { + def javaLauncher = this.javaToolchainService.launcherFor { spec -> + spec.languageVersion.set(this.javaVersion) + }.get() + return this.workerExecutor.processIsolation { spec -> + spec.forkOptions { fork -> + fork.executable = javaLauncher.executablePath + } + } + } +} diff --git a/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/InstrumentWorkParameters.groovy b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/InstrumentWorkParameters.groovy new file mode 100644 index 00000000000..95dfad91916 --- /dev/null +++ b/buildSrc/src/main/groovy/datadog/gradle/plugin/instrument/InstrumentWorkParameters.groovy @@ -0,0 +1,17 @@ +package datadog.gradle.plugin.instrument + + +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.workers.WorkParameters + +interface InstrumentWorkParameters extends WorkParameters { + Property getBuildStartedTime() + ConfigurableFileCollection getPluginClassPath() + ListProperty getPlugins() + ConfigurableFileCollection getInstrumentingClassPath() + DirectoryProperty getCompilerOutputDirectory() + DirectoryProperty getTmpDirectory() +} diff --git a/buildSrc/src/main/kotlin/datadog/gradle/plugin/csi/CallSiteInstrumentationPlugin.kt b/buildSrc/src/main/kotlin/datadog/gradle/plugin/csi/CallSiteInstrumentationPlugin.kt index a0591294843..9fff60f498f 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/csi/CallSiteInstrumentationPlugin.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/csi/CallSiteInstrumentationPlugin.kt @@ -188,14 +188,7 @@ abstract class CallSiteInstrumentationPlugin : Plugin { dependsOn(mainCompileTask) } - // Workaround for instrument plugin modifying compile tasks - project.pluginManager.withPlugin("dd-trace-java.instrument") { - callSiteGeneratorTask.configure { - dependsOn("instrumentJava") - } - } - - // make all sourcesets' class tasks depend on call site generator + // make all sourceSets class tasks depend on call site generator val sourceSets = project.sourceSets sourceSets.named(MAIN_SOURCE_SET_NAME) { project.tasks.named(classesTaskName) { 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 d72074676c3..bbddbd3288d 100644 --- a/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt +++ b/buildSrc/src/main/kotlin/datadog/gradle/plugin/muzzle/MuzzlePlugin.kt @@ -14,7 +14,9 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.Task import org.gradle.api.artifacts.Configuration +import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.TaskProvider +import org.gradle.api.tasks.compile.AbstractCompile import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.exclude import org.gradle.kotlin.dsl.getByType @@ -71,9 +73,8 @@ class MuzzlePlugin : Plugin { // compileMuzzle compiles all projects required to run muzzle validation. // 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 + inputs.files(project.providers.provider { project.allMainSourceSet }) dependsOn(bootstrapProject.tasks.named("compileJava")) dependsOn(bootstrapProject.tasks.named("compileMain_java11Java")) dependsOn(toolingProject.tasks.named("compileJava")) diff --git a/buildSrc/src/test/kotlin/datadog/gradle/plugin/instrument/InstrumentPluginTest.kt b/buildSrc/src/test/kotlin/datadog/gradle/plugin/instrument/BuildTimeInstrumentationPluginTest.kt similarity index 94% rename from buildSrc/src/test/kotlin/datadog/gradle/plugin/instrument/InstrumentPluginTest.kt rename to buildSrc/src/test/kotlin/datadog/gradle/plugin/instrument/BuildTimeInstrumentationPluginTest.kt index 8c7b0b13f0b..aeee3367de7 100644 --- a/buildSrc/src/test/kotlin/datadog/gradle/plugin/instrument/InstrumentPluginTest.kt +++ b/buildSrc/src/test/kotlin/datadog/gradle/plugin/instrument/BuildTimeInstrumentationPluginTest.kt @@ -11,14 +11,14 @@ import org.objectweb.asm.FieldVisitor import java.io.File import java.io.FileInputStream -class InstrumentPluginTest { +class BuildTimeInstrumentationPluginTest { private val buildGradle = """ plugins { id 'java' - id 'dd-trace-java.instrument' + id 'dd-trace-java.build-time-instrumentation' } - + sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -30,13 +30,7 @@ class InstrumentPluginTest { compileOnly group: 'net.bytebuddy', name: 'byte-buddy', version: '1.18.3' // just to build TestPlugin } - configurations { - instrumentPluginClasspath { - canBeResolved = true - } - } - - instrument.plugins = [ + buildTimeInstrumentation.plugins = [ 'TestPlugin' ] """.trimIndent() diff --git a/dd-java-agent/agent-otel/otel-bootstrap/build.gradle b/dd-java-agent/agent-otel/otel-bootstrap/build.gradle index 9349614f1e8..ffa68882dc6 100644 --- a/dd-java-agent/agent-otel/otel-bootstrap/build.gradle +++ b/dd-java-agent/agent-otel/otel-bootstrap/build.gradle @@ -8,7 +8,7 @@ def otelApiVersion = '1.38.0' def otelInstrumentationApiVersion = '2.4.0' apply from: "$rootDir/gradle/java.gradle" -apply plugin: 'dd-trace-java.instrument' +apply plugin: 'dd-trace-java.build-time-instrumentation' configurations { def ec = register('embeddedClasspath') { @@ -19,14 +19,14 @@ configurations { named('compileClasspath') { compileClasspath.extendsFrom(ec.get()) } - instrumentPluginClasspath { + named('buildTimeInstrumentationPlugin') { visible = false canBeConsumed = false canBeResolved = true } } -instrument.plugins = ['datadog.opentelemetry.tooling.shim.OtelShimGradlePlugin'] +buildTimeInstrumentation.plugins = ['datadog.opentelemetry.tooling.shim.OtelShimGradlePlugin'] minimumInstructionCoverage = 0.0 minimumBranchCoverage = 0.0 @@ -53,7 +53,7 @@ dependencies { compileOnly project(':dd-java-agent:agent-bootstrap') implementation project(':dd-java-agent:agent-otel:otel-shim') - instrumentPluginClasspath project(path: ':dd-java-agent:agent-otel:otel-tooling', configuration: 'instrumentPluginClasspath') + buildTimeInstrumentationPlugin project(path: ':dd-java-agent:agent-otel:otel-tooling', configuration: 'buildTimeInstrumentationPlugin') } // unpack embeddedClasspath to same path as compiled classes so it can get instrumented diff --git a/dd-java-agent/agent-otel/otel-tooling/build.gradle b/dd-java-agent/agent-otel/otel-tooling/build.gradle index 8637c9d2581..3cf9eadfb4b 100644 --- a/dd-java-agent/agent-otel/otel-tooling/build.gradle +++ b/dd-java-agent/agent-otel/otel-tooling/build.gradle @@ -4,7 +4,7 @@ minimumInstructionCoverage = 0.0 minimumBranchCoverage = 0.0 configurations { - instrumentPluginClasspath { + buildTimeInstrumentationPlugin { canBeConsumed = true canBeResolved = false extendsFrom runtimeElements diff --git a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/shim/OtelShimGradlePlugin.java b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/shim/OtelShimGradlePlugin.java index f23f446983d..6477e8fcbc5 100644 --- a/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/shim/OtelShimGradlePlugin.java +++ b/dd-java-agent/agent-otel/otel-tooling/src/main/java/datadog/opentelemetry/tooling/shim/OtelShimGradlePlugin.java @@ -13,7 +13,7 @@ /** * Bytebuddy gradle plugin which injects our OpenTelemetry shim into the target API. * - * @see "buildSrc/src/main/groovy/InstrumentPlugin.groovy" + * @see datadog.gradle.plugin.instrument.BuildTimeInstrumentationPlugin */ public class OtelShimGradlePlugin extends Plugin.ForElementMatcher { private final File targetDir; diff --git a/dd-java-agent/agent-tooling/build.gradle b/dd-java-agent/agent-tooling/build.gradle index ce04310fd10..2417efead39 100644 --- a/dd-java-agent/agent-tooling/build.gradle +++ b/dd-java-agent/agent-tooling/build.gradle @@ -28,7 +28,7 @@ sourceSets { } configurations { - register("instrumentPluginClasspath") { + register("buildTimeInstrumentationPlugin") { canBeConsumed = true canBeResolved = false extendsFrom runtimeElements diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/NewTaskForGradlePlugin.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/NewTaskForGradlePlugin.java index 958c7a30cce..2a4a6cf2f8e 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/NewTaskForGradlePlugin.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/NewTaskForGradlePlugin.java @@ -12,7 +12,14 @@ /** * Bytebuddy gradle plugin which rewrites placeholder calls in 'NewTaskFor' advice at compile time. * - * @see "buildSrc/src/main/groovy/InstrumentPlugin.groovy" + *

Used for {@code + * datadog.trace.instrumentation.java.concurrent.WrapRunnableAsNewTaskInstrumentation} + * instrumentation, to replace {@code + * datadog.trace.bootstrap.instrumentation.java.concurrent.NewTaskForPlaceholder#newTaskFor} by + * {@code java.util.concurrent.AbstractExecutorService#newTaskFor(java.lang.Runnable, T)} which is + * protected and not accessible during compilation. + * + * @see datadog.gradle.plugin.instrument.BuildTimeInstrumentationPlugin */ public class NewTaskForGradlePlugin extends Plugin.ForElementMatcher { private final File targetDir; diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/reqctx/RewriteRequestContextAdvicePlugin.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/reqctx/RewriteRequestContextAdvicePlugin.java index 764d8fb04ba..4c86b5db07f 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/reqctx/RewriteRequestContextAdvicePlugin.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/bytebuddy/reqctx/RewriteRequestContextAdvicePlugin.java @@ -9,6 +9,12 @@ import net.bytebuddy.dynamic.DynamicType; import net.bytebuddy.matcher.ElementMatchers; +/** + * Bytebuddy gradle plugin which injects request context handling into classes annotated with {@link + * RequiresRequestContext}. + * + * @see datadog.gradle.plugin.instrument.BuildTimeInstrumentationPlugin + */ public class RewriteRequestContextAdvicePlugin extends Plugin.ForElementMatcher { public RewriteRequestContextAdvicePlugin(File targetDir) { super(ElementMatchers.isAnnotatedWith(RequiresRequestContext.class)); diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGradlePlugin.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGradlePlugin.java index 7faccd70a8f..5a22ac484be 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGradlePlugin.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/muzzle/MuzzleGradlePlugin.java @@ -17,7 +17,7 @@ /** * Byte-Buddy gradle plugin which creates muzzle-references at compile time. * - * @see "buildSrc/src/main/groovy/InstrumentPlugin.groovy" + * @see datadog.gradle.plugin.instrument.BuildTimeInstrumentationPlugin */ public class MuzzleGradlePlugin extends Plugin.ForElementMatcher { static { diff --git a/dd-java-agent/instrumentation/build.gradle b/dd-java-agent/instrumentation/build.gradle index 96479c0f6d2..ea9b3a61793 100644 --- a/dd-java-agent/instrumentation/build.gradle +++ b/dd-java-agent/instrumentation/build.gradle @@ -2,6 +2,7 @@ import static org.gradle.api.plugins.JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAM import static org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import datadog.gradle.plugin.instrument.BuildTimeInstrumentationExtension plugins { id 'com.gradleup.shadow' @@ -14,8 +15,8 @@ tasks.register("latestDepTest", Test) Project parent_project = project subprojects { Project subProj -> - subProj.pluginManager.withPlugin("dd-trace-java.instrument") { - subProj.extensions.configure(InstrumentExtension) { + subProj.pluginManager.withPlugin("dd-trace-java.build-time-instrumentation") { + subProj.extensions.configure(BuildTimeInstrumentationExtension) { it.plugins.addAll( 'datadog.trace.agent.tooling.muzzle.MuzzleGradlePlugin', 'datadog.trace.agent.tooling.bytebuddy.NewTaskForGradlePlugin', @@ -23,12 +24,15 @@ subprojects { Project subProj -> ) } - subProj.configurations.register("instrumentPluginClasspath") { + subProj.configurations.named('buildTimeInstrumentationPlugin') { it.visible = false it.canBeConsumed = false it.canBeResolved = true - it.dependencies.add(subProj.dependencies.project(path: ':dd-java-agent:agent-tooling', configuration: 'instrumentPluginClasspath')) + it.dependencies.add(subProj.dependencies.project( + path: ':dd-java-agent:agent-tooling', + configuration: 'buildTimeInstrumentationPlugin' + )) } } @@ -86,7 +90,7 @@ subprojects { Project subProj -> } } - subProj.apply plugin: 'dd-trace-java.instrument' + subProj.apply plugin: 'dd-trace-java.build-time-instrumentation' subProj.apply plugin: 'dd-trace-java.muzzle' }