Skip to content

Commit 6d6cd85

Browse files
authored
feat(gradle/plugin): native kotlin support (#1925)
1 parent fe2f8d0 commit 6d6cd85

3 files changed

Lines changed: 249 additions & 0 deletions

File tree

byte-buddy-gradle-plugin/src/main/java/net/bytebuddy/build/gradle/AbstractByteBuddyTaskConfiguration.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ protected AbstractByteBuddyTaskConfiguration(String name, SourceSet sourceSet) {
6565
* {@inheritDoc}
6666
*/
6767
public void execute(Project project) {
68+
if (sourceSet.getJava().isEmpty()) {
69+
project.getLogger().debug("Skipping byteBuddy task for source set '{}' as no Java sources are present", sourceSet.getName());
70+
return;
71+
}
6872
@SuppressWarnings("unchecked")
6973
S extension = (S) project.getExtensions().getByName(name);
7074
if (extension.getTransformations().isEmpty() && (extension.getDiscovery() == Discovery.NONE || extension.isEmptyDiscovery())) {

byte-buddy-gradle-plugin/src/main/java/net/bytebuddy/build/gradle/ByteBuddyPlugin.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.gradle.api.Plugin;
2222
import org.gradle.api.Project;
2323
import org.gradle.api.file.SourceDirectorySet;
24+
import org.gradle.api.plugins.AppliedPlugin;
2425
import org.gradle.api.plugins.JavaPlugin;
2526
import org.gradle.api.tasks.SourceSet;
2627
import org.gradle.api.tasks.SourceSetContainer;
@@ -70,6 +71,9 @@ public void apply(Project project) {
7071
} else {
7172
project.getLogger().debug("Applying Byte Buddy plugin (legacy mode: {})", DISPATCHER instanceof Dispatcher.ForLegacyGradle);
7273
project.getPlugins().withType(JavaPlugin.class, new JavaPluginConfigurationAction(project));
74+
if (KotlinByteBuddyTaskConfiguration.isAvailable()) {
75+
project.getPluginManager().withPlugin("org.jetbrains.kotlin.jvm", new KotlinPluginConfigurationAction(project));
76+
}
7377
}
7478
}
7579

@@ -120,6 +124,46 @@ public void execute(JavaPlugin plugin) {
120124
}
121125
}
122126

127+
/**
128+
* An action to configure the Kotlin plugin to apply transformations. Reuses the extension
129+
* registered per source set by {@link JavaPluginConfigurationAction}, so a single
130+
* {@code byteBuddy} DSL block drives both Java and Kotlin Byte Buddy tasks.
131+
*/
132+
protected static class KotlinPluginConfigurationAction implements Action<AppliedPlugin> {
133+
134+
/**
135+
* The Gradle project.
136+
*/
137+
private final Project project;
138+
139+
/**
140+
* Creates a Kotlin plugin configuration action.
141+
*
142+
* @param project The Gradle project.
143+
*/
144+
protected KotlinPluginConfigurationAction(Project project) {
145+
this.project = project;
146+
}
147+
148+
/**
149+
* {@inheritDoc}
150+
*/
151+
public void execute(AppliedPlugin plugin) {
152+
project.getLogger().debug("Kotlin plugin was discovered for modification: {}", plugin.getId());
153+
JavaConventionConfiguration configuration = JavaConventionConfiguration.of(project);
154+
if (configuration == null) {
155+
project.getLogger().warn("Skipping implicit Byte Buddy Kotlin task configuration since Java plugin did not register Java plugin convention or extension");
156+
return;
157+
}
158+
for (SourceSet sourceSet : configuration.getSourceSets()) {
159+
String name = sourceSet.getName().equals(SourceSet.MAIN_SOURCE_SET_NAME)
160+
? "byteBuddy"
161+
: (sourceSet.getName() + "ByteBuddy");
162+
project.afterEvaluate(new KotlinByteBuddyTaskConfiguration(name, sourceSet));
163+
}
164+
}
165+
}
166+
123167
/**
124168
* A dispatcher for creating Gradle integrations depending on the available API.
125169
*
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Copyright 2014 - Present Rafael Winterhalter
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package net.bytebuddy.build.gradle;
17+
18+
import net.bytebuddy.utility.nullability.MaybeNull;
19+
import org.gradle.api.Action;
20+
import org.gradle.api.GradleException;
21+
import org.gradle.api.Project;
22+
import org.gradle.api.Task;
23+
import org.gradle.api.execution.TaskExecutionGraph;
24+
import org.gradle.api.file.DirectoryProperty;
25+
import org.gradle.api.file.FileCollection;
26+
import org.gradle.api.file.SourceDirectorySet;
27+
import org.gradle.api.tasks.SourceSet;
28+
import org.gradle.api.tasks.compile.AbstractCompile;
29+
30+
import java.lang.reflect.Method;
31+
32+
/**
33+
* Configures a Byte Buddy task that transforms the output of a Kotlin compilation. Reuses the
34+
* Byte Buddy extension registered under {@code extensionName} by
35+
* {@link ByteBuddyPlugin.JavaPluginConfigurationAction} so a single {@code byteBuddy} DSL block
36+
* applies to both Java and Kotlin outputs.
37+
*
38+
* <p>If the Kotlin Gradle plugin is not on the classpath, or the source set has no Kotlin sources
39+
* or no {@code compile{SourceSet}Kotlin} task, this action is a no-op. Depends on the Kotlin
40+
* Gradle plugin API only via reflection so the Byte Buddy Gradle plugin remains usable in
41+
* Java-only projects.</p>
42+
*/
43+
public class KotlinByteBuddyTaskConfiguration implements Action<Project> {
44+
45+
/**
46+
* The {@code org.jetbrains.kotlin.gradle.tasks.KotlinCompileTool} interface, or
47+
* {@code null} if the Kotlin Gradle plugin API is not on the classpath.
48+
*/
49+
@MaybeNull
50+
private static final Class<?> KOTLIN_COMPILE_TOOL;
51+
52+
/**
53+
* The {@code KotlinCompileTool#getDestinationDirectory} method, or {@code null}.
54+
*/
55+
@MaybeNull
56+
private static final Method GET_DESTINATION_DIRECTORY;
57+
58+
/**
59+
* The {@code KotlinCompileTool#getLibraries} method, or {@code null}.
60+
*/
61+
@MaybeNull
62+
private static final Method GET_LIBRARIES;
63+
64+
/**
65+
* The {@code SourceDirectorySet#getDestinationDirectory} method, or {@code null} on legacy Gradle.
66+
*/
67+
@MaybeNull
68+
private static final Method GET_DESTINATION_DIRECTORY_SOURCE;
69+
70+
/*
71+
* Resolves Kotlin Gradle plugin API entry points, if available.
72+
*/
73+
static {
74+
Class<?> kotlinCompileTool;
75+
Method getDestinationDirectory, getLibraries, getDestinationDirectorySource;
76+
try {
77+
kotlinCompileTool = Class.forName("org.jetbrains.kotlin.gradle.tasks.KotlinCompileTool");
78+
getDestinationDirectory = kotlinCompileTool.getMethod("getDestinationDirectory");
79+
getLibraries = kotlinCompileTool.getMethod("getLibraries");
80+
} catch (Throwable ignored) {
81+
kotlinCompileTool = null;
82+
getDestinationDirectory = null;
83+
getLibraries = null;
84+
}
85+
try {
86+
getDestinationDirectorySource = SourceDirectorySet.class.getMethod("getDestinationDirectory");
87+
} catch (Throwable ignored) {
88+
getDestinationDirectorySource = null;
89+
}
90+
KOTLIN_COMPILE_TOOL = kotlinCompileTool;
91+
GET_DESTINATION_DIRECTORY = getDestinationDirectory;
92+
GET_LIBRARIES = getLibraries;
93+
GET_DESTINATION_DIRECTORY_SOURCE = getDestinationDirectorySource;
94+
}
95+
96+
/**
97+
* Returns {@code true} if the Kotlin Gradle plugin API is available on the classpath and the
98+
* running Gradle version exposes {@code SourceDirectorySet#getDestinationDirectory}.
99+
*
100+
* @return {@code true} if Kotlin support can be wired.
101+
*/
102+
public static boolean isAvailable() {
103+
return KOTLIN_COMPILE_TOOL != null && GET_DESTINATION_DIRECTORY_SOURCE != null;
104+
}
105+
106+
/**
107+
* The name of the shared Byte Buddy extension, as registered by
108+
* {@link ByteBuddyPlugin.JavaPluginConfigurationAction}.
109+
*/
110+
private final String extensionName;
111+
112+
/**
113+
* The source set for which the Kotlin Byte Buddy task is being configured.
114+
*/
115+
private final SourceSet sourceSet;
116+
117+
/**
118+
* Creates a new Kotlin Byte Buddy task configuration.
119+
*
120+
* @param extensionName The name of the shared Byte Buddy extension.
121+
* @param sourceSet The source set for which the task chain is being configured.
122+
*/
123+
protected KotlinByteBuddyTaskConfiguration(String extensionName, SourceSet sourceSet) {
124+
this.extensionName = extensionName;
125+
this.sourceSet = sourceSet;
126+
}
127+
128+
/**
129+
* {@inheritDoc}
130+
*/
131+
@SuppressWarnings({"rawtypes", "unchecked"})
132+
public void execute(Project project) {
133+
if (!isAvailable()) {
134+
return;
135+
}
136+
String kotlinCompileTaskName = sourceSet.getCompileTaskName("kotlin");
137+
Task compileTask = project.getTasks().findByName(kotlinCompileTaskName);
138+
if (compileTask == null || !KOTLIN_COMPILE_TOOL.isInstance(compileTask)) {
139+
project.getLogger().debug("Skipping Kotlin Byte Buddy configuration for source set '{}': no compatible Kotlin compile task named '{}'",
140+
sourceSet.getName(), kotlinCompileTaskName);
141+
return;
142+
}
143+
Object kotlinSources = sourceSet.getExtensions().findByName("kotlin");
144+
if (!(kotlinSources instanceof SourceDirectorySet)) {
145+
project.getLogger().debug("Skipping Kotlin Byte Buddy configuration for source set '{}': no 'kotlin' SourceDirectorySet extension",
146+
sourceSet.getName());
147+
return;
148+
}
149+
AbstractByteBuddyTaskExtension extension = (AbstractByteBuddyTaskExtension) project.getExtensions().findByName(extensionName);
150+
if (extension == null) {
151+
project.getLogger().debug("Skipping Kotlin Byte Buddy configuration for source set '{}': extension '{}' not registered",
152+
sourceSet.getName(), extensionName);
153+
return;
154+
}
155+
if (extension.getTransformations().isEmpty() && (extension.getDiscovery() == Discovery.NONE || extension.isEmptyDiscovery())) {
156+
project.getLogger().debug("Not configuring Kotlin Byte Buddy task for source set '{}' as no transformations are defined and discovery is disabled",
157+
sourceSet.getName());
158+
return;
159+
}
160+
String taskName = extensionName + "Kotlin";
161+
project.getLogger().debug("Configuring Byte Buddy Kotlin task for source set '{}' as '{}'", sourceSet.getName(), taskName);
162+
ByteBuddyTask byteBuddyTask = (ByteBuddyTask) project.getTasks().create(taskName, extension.toType());
163+
byteBuddyTask.setGroup("Byte Buddy");
164+
byteBuddyTask.setDescription("Transforms the classes compiled by " + compileTask.getName());
165+
byteBuddyTask.dependsOn(compileTask);
166+
extension.configure(byteBuddyTask);
167+
configureDirectories((SourceDirectorySet) kotlinSources, compileTask, byteBuddyTask);
168+
Task compileJavaTask = project.getTasks().findByName(sourceSet.getCompileJavaTaskName());
169+
if (compileJavaTask instanceof AbstractCompile) {
170+
byteBuddyTask.dependsOn(compileJavaTask);
171+
byteBuddyTask.getClassPath().from(((AbstractCompile) compileJavaTask).getDestinationDirectory());
172+
}
173+
Task classesTask = project.getTasks().findByName(sourceSet.getClassesTaskName());
174+
if (classesTask != null) {
175+
classesTask.dependsOn(byteBuddyTask);
176+
}
177+
}
178+
179+
/**
180+
* Mirrors the raw-directory redirection performed for Java compilations onto the Kotlin
181+
* compile task: the Kotlin compile task writes to a sibling {@code kotlinByteBuddyRaw}
182+
* folder which the Byte Buddy task reads and then writes back to the original destination.
183+
*
184+
* @param source The Kotlin source directory set.
185+
* @param compileTask The Kotlin compile task (a {@code KotlinCompileTool}).
186+
* @param byteBuddyTask The Byte Buddy task consuming the compilation output.
187+
*/
188+
private static void configureDirectories(SourceDirectorySet source, Task compileTask, ByteBuddyTask byteBuddyTask) {
189+
try {
190+
DirectoryProperty directory = (DirectoryProperty) GET_DESTINATION_DIRECTORY_SOURCE.invoke(source);
191+
String rawPath = "../kotlin" + AbstractByteBuddyTaskConfiguration.RAW_FOLDER_SUFFIX;
192+
((DirectoryProperty) GET_DESTINATION_DIRECTORY.invoke(compileTask)).set(directory.dir(rawPath));
193+
byteBuddyTask.getSource().set(directory.dir(rawPath));
194+
byteBuddyTask.getTarget().set(directory);
195+
byteBuddyTask.getClassPath().from((FileCollection) GET_LIBRARIES.invoke(compileTask));
196+
} catch (Exception exception) {
197+
throw new GradleException("Could not adjust directories for Kotlin Byte Buddy task", exception);
198+
}
199+
}
200+
201+
}

0 commit comments

Comments
 (0)