Skip to content

Commit 21d9291

Browse files
committed
chore(build): Rewrite InstrumentPlugin as compile post processing action
The current implementation was using bad practices in various ways, `project.afterEvaluate`, task creation, explicit `dependsOn`
1 parent d8b9ae8 commit 21d9291

File tree

6 files changed

+169
-103
lines changed

6 files changed

+169
-103
lines changed

buildSrc/src/main/groovy/InstrumentPlugin.groovy

Lines changed: 161 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
import org.gradle.api.DefaultTask
1+
import org.gradle.api.Action
22
import org.gradle.api.Plugin
33
import org.gradle.api.Project
44
import org.gradle.api.file.ConfigurableFileCollection
55
import org.gradle.api.file.Directory
66
import org.gradle.api.file.DirectoryProperty
77
import org.gradle.api.invocation.BuildInvocationDetails
8+
import org.gradle.api.logging.Logger
9+
import org.gradle.api.logging.Logging
810
import org.gradle.api.provider.ListProperty
911
import org.gradle.api.provider.Property
10-
import org.gradle.api.tasks.Classpath
11-
import org.gradle.api.tasks.Input
12-
import org.gradle.api.tasks.InputDirectory
13-
import org.gradle.api.tasks.InputFiles
14-
import org.gradle.api.tasks.Optional
15-
import org.gradle.api.tasks.OutputDirectory
16-
import org.gradle.api.tasks.TaskAction
12+
import org.gradle.api.provider.Provider
13+
import org.gradle.api.tasks.SourceSet
14+
import org.gradle.api.tasks.SourceSetContainer
1715
import org.gradle.api.tasks.compile.AbstractCompile
1816
import org.gradle.jvm.toolchain.JavaLanguageVersion
1917
import org.gradle.jvm.toolchain.JavaToolchainService
@@ -22,6 +20,9 @@ import org.gradle.workers.WorkParameters
2220
import org.gradle.workers.WorkerExecutor
2321

2422
import javax.inject.Inject
23+
import java.nio.file.Files
24+
import java.nio.file.Path
25+
import java.nio.file.StandardCopyOption
2526
import java.util.concurrent.ConcurrentHashMap
2627
import java.util.regex.Matcher
2728

@@ -30,18 +31,53 @@ import java.util.regex.Matcher
3031
*/
3132
@SuppressWarnings('unused')
3233
class InstrumentPlugin implements Plugin<Project> {
34+
public static final String DEFAULT_JAVA_VERSION = 'default'
35+
private final Logger logger = Logging.getLogger(InstrumentPlugin)
36+
3337
@Override
3438
void apply(Project project) {
3539
InstrumentExtension extension = project.extensions.create('instrument', InstrumentExtension)
3640

37-
project.tasks.matching {
38-
it.name in ['compileJava', 'compileScala', 'compileKotlin', 'compileGroovy'] ||
39-
it.name =~ /compileMain_.+Java/
40-
}.all {
41-
AbstractCompile compileTask = it as AbstractCompile
42-
Matcher versionMatcher = it.name =~ /compileMain_(.+)Java/
43-
project.afterEvaluate {
44-
if (!compileTask.source.empty) {
41+
project.pluginManager.withPlugin("java") {configurePostCompilationInstrumentation("java", project, extension) }
42+
project.pluginManager.withPlugin("kotlin") {configurePostCompilationInstrumentation("kotlin", project, extension) }
43+
project.pluginManager.withPlugin("scala") {configurePostCompilationInstrumentation("scala", project, extension) }
44+
project.pluginManager.withPlugin("groovy") {configurePostCompilationInstrumentation("groovy", project, extension) }
45+
}
46+
47+
private void configurePostCompilationInstrumentation(String language, Project project, InstrumentExtension extension) {
48+
project.extensions.configure(SourceSetContainer) { SourceSetContainer sourceSets ->
49+
// For any "main" source-set configure its compile task
50+
sourceSets.configureEach { SourceSet sourceSet ->
51+
def sourceSetName = sourceSet.name
52+
logger.info("[InstrumentPlugin] source-set: $sourceSetName, language: $language")
53+
54+
if (!sourceSetName.startsWith(SourceSet.MAIN_SOURCE_SET_NAME)) {
55+
logger.debug("[InstrumentPlugin] Skipping non-main source set {} for language {}", sourceSetName, language)
56+
return
57+
}
58+
59+
def compileTaskName = sourceSet.getCompileTaskName(language)
60+
logger.info("[InstrumentPlugin] compile task name: " + compileTaskName)
61+
62+
// For each compile task, append an instrumenting post-processing step
63+
// Examples of compile tasks:
64+
// - compileJava,
65+
// - compileMain_java17Java,
66+
// - compileMain_jetty904Java,
67+
// - compileMain_play25Java,
68+
// - compileEe8TestJava,
69+
// - compileLatestDepTestJava
70+
// - compileKotlin,
71+
// - compileScala,
72+
// - compileGroovy,
73+
project.tasks.named(compileTaskName, AbstractCompile) {
74+
if (it.source.isEmpty()) {
75+
logger.info("[InstrumentPlugin] Skipping $compileTaskName for source set $sourceSetName as it has no source files")
76+
return
77+
}
78+
79+
// Compute optional Java version
80+
Matcher versionMatcher = compileTaskName =~ /compileMain_(.+)Java/
4581
String sourceSetSuffix = null
4682
String javaVersion = null
4783
if (versionMatcher.matches()) {
@@ -50,64 +86,56 @@ class InstrumentPlugin implements Plugin<Project> {
5086
javaVersion = sourceSetSuffix[4..-1]
5187
}
5288
}
89+
javaVersion = javaVersion ?: DEFAULT_JAVA_VERSION // null not accepted
90+
it.inputs.property("javaVersion", javaVersion)
5391

54-
// insert intermediate 'raw' directory for unprocessed classes
55-
Directory classesDir = compileTask.destinationDirectory.get()
56-
Directory rawClassesDir = classesDir.dir(
57-
"../raw${sourceSetSuffix ? "_$sourceSetSuffix" : ''}/")
58-
compileTask.destinationDirectory.set(rawClassesDir.asFile)
59-
60-
// insert task between compile and jar, and before test*
61-
String instrumentTaskName = compileTask.name.replace('compile', 'instrument')
62-
def instrumentTask = project.tasks.register(instrumentTaskName, InstrumentTask) {
63-
// Task configuration
64-
it.group = 'Byte Buddy'
65-
it.description = "Instruments the classes compiled by ${compileTask.name}"
66-
it.inputs.dir(compileTask.destinationDirectory)
67-
it.outputs.dir(classesDir)
68-
// Task inputs
69-
it.javaVersion = javaVersion
70-
def instrumenterConfiguration = project.configurations.named('instrumentPluginClasspath')
71-
if (instrumenterConfiguration.present) {
72-
it.pluginClassPath.from(instrumenterConfiguration.get())
73-
}
74-
it.plugins = extension.plugins
75-
it.instrumentingClassPath.from(
76-
findCompileClassPath(project, it.name) +
77-
rawClassesDir +
78-
findAdditionalClassPath(extension, it.name)
79-
)
80-
it.sourceDirectory = rawClassesDir
81-
// Task output
82-
it.targetDirectory = classesDir
83-
}
84-
if (javaVersion) {
85-
project.tasks.named(project.sourceSets."main_java${javaVersion}".classesTaskName) {
86-
it.dependsOn(instrumentTask)
87-
}
88-
} else {
89-
project.tasks.named(project.sourceSets.main.classesTaskName) {
90-
it.dependsOn(instrumentTask)
91-
}
92+
def plugins = extension.plugins
93+
it.inputs.property("plugins", plugins)
94+
95+
def pluginClassPath = project.objects.fileCollection()
96+
def instrumentConfiguration = project.configurations.findByName('instrumentPluginClasspath')
97+
if (instrumentConfiguration != null) {
98+
pluginClassPath.from(instrumentConfiguration)
99+
it.inputs.files(pluginClassPath)
92100
}
101+
102+
def compileTaskClasspath = it.classpath
103+
def rawClassesDir = it.destinationDirectory
104+
105+
def additionalClassPath = findAdditionalClassPath(extension, it.name)
106+
it.inputs.files(additionalClassPath)
107+
108+
def instrumentingClassPath = project.objects.fileCollection()
109+
instrumentingClassPath.setFrom(
110+
compileTaskClasspath,
111+
rawClassesDir,
112+
additionalClassPath,
113+
)
114+
115+
// This were the post processing happens, i.e. were the instrumentation is applied
116+
it.doLast(
117+
"instrumentClasses",
118+
project.objects.newInstance(
119+
InstrumentPostProcessingAction,
120+
javaVersion,
121+
plugins,
122+
pluginClassPath,
123+
instrumentingClassPath,
124+
rawClassesDir,
125+
)
126+
)
127+
logger.info("[InstrumentPlugin] Configured post-compile instrumentation for $compileTaskName for source-set $sourceSetName")
93128
}
94129
}
95130
}
96131
}
97132

98-
static findCompileClassPath(Project project, String taskName) {
99-
def matcher = taskName =~ /instrument([A-Z].+)Java/
100-
def cfgName = matcher.matches() ? "${matcher.group(1).uncapitalize()}CompileClasspath" : 'compileClasspath'
101-
project.configurations.named(cfgName).findAll {
102-
it.name != 'previous-compilation-data.bin' && !it.name.endsWith('.gz')
103-
}
104-
}
105-
106-
static findAdditionalClassPath(InstrumentExtension extension, String taskName) {
107-
extension.additionalClasspath.getOrDefault(taskName, []).collect {
108-
// insert intermediate 'raw' directory for unprocessed classes
109-
def fileName = it.get().asFile.name
110-
it.get().dir("../${fileName.replaceFirst('^main', 'raw')}")
133+
private static List<Provider<Directory>> findAdditionalClassPath(InstrumentExtension extension, String taskName) {
134+
return extension.additionalClasspath.getOrDefault(taskName, []).collect { dirProperty ->
135+
dirProperty.map {
136+
def fileName = it.asFile.name
137+
it.dir("../${fileName.replaceFirst('^main', 'raw')}")
138+
}
111139
}
112140
}
113141
}
@@ -117,42 +145,60 @@ abstract class InstrumentExtension {
117145
Map<String /* taskName */, List<DirectoryProperty>> additionalClasspath = [:]
118146
}
119147

120-
abstract class InstrumentTask extends DefaultTask {
121-
@Input @Optional
122-
String javaVersion
123-
@InputFiles @Classpath
124-
abstract ConfigurableFileCollection getPluginClassPath()
125-
@Input
126-
ListProperty<String> plugins
127-
@InputFiles @Classpath
128-
abstract ConfigurableFileCollection getInstrumentingClassPath()
129-
@InputDirectory
130-
Directory sourceDirectory
131-
132-
@OutputDirectory
133-
Directory targetDirectory
134-
148+
abstract class InstrumentPostProcessingAction implements Action<AbstractCompile> {
149+
@Inject
150+
abstract Project getProject()
135151
@Inject
136152
abstract JavaToolchainService getJavaToolchainService()
137153
@Inject
138154
abstract BuildInvocationDetails getInvocationDetails()
139155
@Inject
140156
abstract WorkerExecutor getWorkerExecutor()
141157

142-
@TaskAction
143-
instrument() {
158+
final String javaVersion
159+
final ListProperty<String> plugins
160+
final ConfigurableFileCollection pluginClassPath
161+
final ConfigurableFileCollection instrumentingClassPath
162+
final DirectoryProperty rawClassesDirectory
163+
164+
@Inject
165+
InstrumentPostProcessingAction(
166+
String javaVersion,
167+
ListProperty<String> plugins,
168+
ConfigurableFileCollection pluginClassPath,
169+
ConfigurableFileCollection instrumentingClassPath,
170+
DirectoryProperty rawClassesDir
171+
) {
172+
this.javaVersion = javaVersion
173+
this.plugins = plugins
174+
this.pluginClassPath = pluginClassPath
175+
this.instrumentingClassPath = instrumentingClassPath
176+
this.rawClassesDirectory = rawClassesDir
177+
}
178+
179+
@Override
180+
void execute(AbstractCompile task) {
181+
logger.info(
182+
"values: " +
183+
"javaVersion=${javaVersion}, " +
184+
"plugins=${plugins.get()}, " +
185+
"pluginClassPath=${pluginClassPath.files}, " +
186+
"instrumentingClassPath=${instrumentingClassPath.files}, " +
187+
"rawClassesDirectory=${rawClassesDirectory.get().asFile}"
188+
)
189+
def postCompileAction = this
144190
workQueue().submit(InstrumentAction.class, parameters -> {
145-
parameters.buildStartedTime.set(this.invocationDetails.buildStartedTime)
146-
parameters.pluginClassPath.from(this.pluginClassPath)
147-
parameters.plugins.set(this.plugins)
148-
parameters.instrumentingClassPath.setFrom(this.instrumentingClassPath)
149-
parameters.sourceDirectory.set(this.sourceDirectory.asFile)
150-
parameters.targetDirectory.set(this.targetDirectory.asFile)
191+
parameters.buildStartedTime.set(invocationDetails.buildStartedTime)
192+
parameters.pluginClassPath.from(postCompileAction.pluginClassPath)
193+
parameters.plugins.set(postCompileAction.plugins)
194+
parameters.instrumentingClassPath.setFrom(postCompileAction.instrumentingClassPath)
195+
parameters.classesDirectory.set(postCompileAction.rawClassesDirectory)
196+
parameters.tmpDirectory.set(project.layout.buildDirectory.dir("tmp/${task.name}-raw-classes"))
151197
})
152198
}
153199

154200
private workQueue() {
155-
if (this.javaVersion) {
201+
if (this.javaVersion != InstrumentPlugin.DEFAULT_JAVA_VERSION) {
156202
def javaLauncher = this.javaToolchainService.launcherFor { spec ->
157203
spec.languageVersion.set(JavaLanguageVersion.of(this.javaVersion))
158204
}.get()
@@ -172,8 +218,8 @@ interface InstrumentWorkParameters extends WorkParameters {
172218
ConfigurableFileCollection getPluginClassPath()
173219
ListProperty<String> getPlugins()
174220
ConfigurableFileCollection getInstrumentingClassPath()
175-
DirectoryProperty getSourceDirectory()
176-
DirectoryProperty getTargetDirectory()
221+
DirectoryProperty getClassesDirectory()
222+
DirectoryProperty getTmpDirectory()
177223
}
178224

179225
abstract class InstrumentAction implements WorkAction<InstrumentWorkParameters> {
@@ -199,10 +245,29 @@ abstract class InstrumentAction implements WorkAction<InstrumentWorkParameters>
199245
}
200246
}
201247
}
202-
File sourceDirectory = parameters.getSourceDirectory().get().asFile
203-
File targetDirectory = parameters.getTargetDirectory().get().asFile
248+
Path classesDirectory = parameters.classesDirectory.get().asFile.toPath()
249+
Path tmpSourceDir = parameters.tmpDirectory.get().asFile.toPath()
250+
251+
// Delete tmpSourceDir contents recursively
252+
if (tmpSourceDir.exists()) {
253+
Files.walk(tmpSourceDir)
254+
.sorted(Comparator.reverseOrder())
255+
.forEach { p ->
256+
if (!p.equals(tmpSourceDir)) {
257+
java.nio.file.Files.deleteIfExists(p)
258+
}
259+
}
260+
}
261+
262+
Files.move(
263+
classesDirectory,
264+
tmpSourceDir,
265+
StandardCopyOption.REPLACE_EXISTING,
266+
StandardCopyOption.ATOMIC_MOVE,
267+
)
268+
204269
ClassLoader instrumentingCL = createClassLoader(parameters.instrumentingClassPath, pluginCL)
205-
InstrumentingPlugin.instrumentClasses(plugins, instrumentingCL, sourceDirectory, targetDirectory)
270+
InstrumentingPlugin.instrumentClasses(plugins, instrumentingCL, tmpSourceDir, classesDirectory)
206271
}
207272

208273
static ClassLoader createClassLoader(cp, parent = InstrumentAction.classLoader) {

buildSrc/src/main/groovy/MuzzlePlugin.groovy

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import org.gradle.api.model.ObjectFactory
3030
import org.gradle.api.provider.Property
3131
import org.gradle.api.tasks.SourceSet
3232
import org.gradle.api.tasks.TaskProvider
33+
import org.gradle.api.tasks.compile.AbstractCompile
3334
import org.gradle.jvm.toolchain.JavaLanguageVersion
3435
import org.gradle.jvm.toolchain.JavaToolchainService
3536
import org.gradle.workers.WorkAction
@@ -111,7 +112,8 @@ class MuzzlePlugin implements Plugin<Project> {
111112
// compileMuzzle compiles all projects required to run muzzle validation.
112113
// Not adding group and description to keep this task from showing in `gradle tasks`.
113114
TaskProvider<Task> compileMuzzle = project.tasks.register('compileMuzzle') {
114-
it.dependsOn(project.tasks.withType(InstrumentTask))
115+
// TODO fix eagerness, check jetty an co projects
116+
it.dependsOn project.tasks.matching { it instanceof AbstractCompile && it.name.startsWith('') }
115117
it.dependsOn bootstrapProject.tasks.named("compileJava")
116118
it.dependsOn bootstrapProject.tasks.named("compileMain_java11Java")
117119
it.dependsOn toolingProject.tasks.named("compileJava")

buildSrc/src/test/groovy/InstrumentPluginTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class InstrumentPluginTest extends Specification {
2626
compileOnly group: 'net.bytebuddy', name: 'byte-buddy', version: '1.17.5' // just to build TestPlugin
2727
}
2828
29-
apply plugin: 'instrument'
29+
// apply plugin: 'instrument'
3030
3131
instrument.plugins = [
3232
'TestPlugin'

dd-java-agent/instrumentation/jetty-9/build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ List<DirectoryProperty> extraInstrumentJavaDirs = []
9191
extraInstrumentJavaDirs << compileTask.destinationDirectory
9292
compileTask.dependsOn tasks['compileJava']
9393
project.afterEvaluate { p ->
94-
tasks['instrumentJava'].dependsOn compileTask
9594
tasks["forbiddenApis${it.capitalize()}"].dependsOn("instrument${it.capitalize()}Java")
9695
}
9796
}

dd-java-agent/instrumentation/play-2.4/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ jar {
6161
from sourceSets.main_play25.output
6262
}
6363
project.afterEvaluate { p ->
64-
instrumentJava.dependsOn compileMain_play25Java
65-
forbiddenApisMain_play25.dependsOn instrumentMain_play25Java
64+
// instrumentJava.dependsOn compileMain_play25Java // TODO remove
65+
forbiddenApisMain_play25.dependsOn compileMain_play25Java
6666
}
6767
instrument {
6868
additionalClasspath = [

dd-java-agent/instrumentation/play-2.6/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ jar {
7777
}
7878
compileMain_play27Java.dependsOn compileJava
7979
project.afterEvaluate { p ->
80-
instrumentJava.dependsOn compileMain_play27Java
81-
forbiddenApisMain_play27.dependsOn instrumentMain_play27Java
80+
// instrumentJava.dependsOn compileMain_play27Java
81+
forbiddenApisMain_play27.dependsOn compileMain_play27Java
8282
}
8383
instrument {
8484
additionalClasspath = [

0 commit comments

Comments
 (0)