1 /*
<lambda>null2 * Copyright 2017-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3 */
4
5 package kotlinx.atomicfu.plugin.gradle
6
7 import kotlinx.atomicfu.transformer.*
8 import org.gradle.api.*
9 import org.gradle.api.file.*
10 import org.gradle.api.internal.*
11 import org.gradle.api.plugins.*
12 import org.gradle.api.tasks.*
13 import org.gradle.api.tasks.compile.*
14 import org.gradle.api.tasks.testing.*
15 import org.gradle.jvm.tasks.*
16 import org.jetbrains.kotlin.gradle.dsl.*
17 import org.jetbrains.kotlin.gradle.plugin.*
18 import java.io.*
19 import java.util.*
20 import java.util.concurrent.*
21
22 private const val EXTENSION_NAME = "atomicfu"
23 private const val ORIGINAL_DIR_NAME = "originalClassesDir"
24 private const val COMPILE_ONLY_CONFIGURATION = "compileOnly"
25 private const val IMPLEMENTATION_CONFIGURATION = "implementation"
26 private const val TEST_IMPLEMENTATION_CONFIGURATION = "testImplementation"
27
28 open class AtomicFUGradlePlugin : Plugin<Project> {
29 override fun apply(project: Project) = project.run {
30 val pluginVersion = rootProject.buildscript.configurations.findByName("classpath")
31 ?.allDependencies?.find { it.name == "atomicfu-gradle-plugin" }?.version
32 extensions.add(EXTENSION_NAME, AtomicFUPluginExtension(pluginVersion))
33 configureDependencies()
34 configureTasks()
35 }
36 }
37
Projectnull38 private fun Project.configureDependencies() {
39 withPluginWhenEvaluatedDependencies("kotlin") { version ->
40 dependencies.add(
41 if (config.transformJvm) COMPILE_ONLY_CONFIGURATION else IMPLEMENTATION_CONFIGURATION,
42 getAtomicfuDependencyNotation(Platform.JVM, version)
43 )
44 dependencies.add(TEST_IMPLEMENTATION_CONFIGURATION, getAtomicfuDependencyNotation(Platform.JVM, version))
45 }
46 withPluginWhenEvaluatedDependencies("kotlin2js") { version ->
47 dependencies.add(
48 if (config.transformJs) COMPILE_ONLY_CONFIGURATION else IMPLEMENTATION_CONFIGURATION,
49 getAtomicfuDependencyNotation(Platform.JS, version)
50 )
51 dependencies.add(TEST_IMPLEMENTATION_CONFIGURATION, getAtomicfuDependencyNotation(Platform.JS, version))
52 }
53 withPluginWhenEvaluatedDependencies("kotlin-multiplatform") { version ->
54 configureMultiplatformPluginDependencies(version)
55 }
56 }
57
configureTasksnull58 private fun Project.configureTasks() {
59 val config = config
60 withPluginWhenEvaluated("kotlin") {
61 if (config.transformJvm) {
62 configureTransformTasks("compileTestKotlin") { sourceSet, transformedDir, originalDir ->
63 createJvmTransformTask(sourceSet).configureJvmTask(
64 sourceSet.compileClasspath,
65 sourceSet.classesTaskName,
66 transformedDir,
67 originalDir,
68 config
69 )
70 }
71 }
72 }
73 withPluginWhenEvaluated("kotlin2js") {
74 if (config.transformJs) {
75 configureTransformTasks("compileTestKotlin2Js") { sourceSet, transformedDir, originalDir ->
76 createJsTransformTask(sourceSet).configureJsTask(
77 sourceSet.classesTaskName,
78 transformedDir,
79 originalDir,
80 config
81 )
82 }
83 }
84 }
85 withPluginWhenEvaluated("kotlin-multiplatform") {
86 configureMultiplatformPluginTasks()
87 }
88 }
89
90 private enum class Platform(val suffix: String) {
91 JVM("-jvm"),
92 JS("-js"),
93 NATIVE(""),
94 MULTIPLATFORM("")
95 }
96
97 private enum class CompilationType { MAIN, TEST }
98
compilationNameToTypenull99 private fun String.compilationNameToType(): CompilationType? = when (this) {
100 KotlinCompilation.MAIN_COMPILATION_NAME -> CompilationType.MAIN
101 KotlinCompilation.TEST_COMPILATION_NAME -> CompilationType.TEST
102 else -> null
103 }
104
sourceSetNameToTypenull105 private fun String.sourceSetNameToType(): CompilationType? = when (this) {
106 SourceSet.MAIN_SOURCE_SET_NAME -> CompilationType.MAIN
107 SourceSet.TEST_SOURCE_SET_NAME -> CompilationType.TEST
108 else -> null
109 }
110
111 private val Project.config: AtomicFUPluginExtension
112 get() = extensions.findByName(EXTENSION_NAME) as? AtomicFUPluginExtension ?: AtomicFUPluginExtension(null)
113
getAtomicfuDependencyNotationnull114 private fun getAtomicfuDependencyNotation(platform: Platform, version: String): String =
115 "org.jetbrains.kotlinx:atomicfu${platform.suffix}:$version"
116
117 // Note "afterEvaluate" does nothing when the project is already in executed state, so we need
118 // a special check for this case
119 fun <T> Project.whenEvaluated(fn: Project.() -> T) {
120 if (state.executed) {
121 fn()
122 } else {
123 afterEvaluate { fn() }
124 }
125 }
126
Projectnull127 fun Project.withPluginWhenEvaluated(plugin: String, fn: Project.() -> Unit) {
128 pluginManager.withPlugin(plugin) { whenEvaluated(fn) }
129 }
130
Projectnull131 fun Project.withPluginWhenEvaluatedDependencies(plugin: String, fn: Project.(version: String) -> Unit) {
132 withPluginWhenEvaluated(plugin) {
133 config.dependenciesVersion?.let { fn(it) }
134 }
135 }
136
Projectnull137 fun Project.withKotlinTargets(fn: (KotlinTarget) -> Unit) {
138 extensions.findByType(KotlinProjectExtension::class.java)?.let { kotlinExtension ->
139 val targetsExtension = (kotlinExtension as? ExtensionAware)?.extensions?.findByName("targets")
140 @Suppress("UNCHECKED_CAST")
141 val targets = targetsExtension as NamedDomainObjectContainer<KotlinTarget>
142 // find all compilations given sourceSet belongs to
143 targets.all { target -> fn(target) }
144 }
145 }
146
addFriendPathsnull147 private fun KotlinCommonOptions.addFriendPaths(friendPathsFileCollection: FileCollection) {
148 val argName = when (this) {
149 is KotlinJvmOptions -> "-Xfriend-paths"
150 is KotlinJsOptions -> "-Xfriend-modules"
151 else -> return
152 }
153 freeCompilerArgs = freeCompilerArgs + "$argName=${friendPathsFileCollection.joinToString(",")}"
154 }
155
Projectnull156 fun Project.configureMultiplatformPluginTasks() {
157 val originalDirsByCompilation = hashMapOf<KotlinCompilation<*>, FileCollection>()
158 val config = config
159 withKotlinTargets { target ->
160 if (target.platformType == KotlinPlatformType.common || target.platformType == KotlinPlatformType.native) {
161 return@withKotlinTargets // skip the common & native targets -- no transformation for them
162 }
163 target.compilations.all compilations@{ compilation ->
164 val compilationType = compilation.name.compilationNameToType()
165 ?: return@compilations // skip unknown compilations
166 val classesDirs = compilation.output.classesDirs
167 // make copy of original classes directory
168 val originalClassesDirs: FileCollection =
169 project.files(classesDirs.from.toTypedArray()).filter { it.exists() }
170 originalDirsByCompilation[compilation] = originalClassesDirs
171 val transformedClassesDir =
172 project.buildDir.resolve("classes/atomicfu/${target.name}/${compilation.name}")
173 val transformTask = when (target.platformType) {
174 KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> {
175 if (!config.transformJvm) return@compilations // skip when transformation is turned off
176 project.createJvmTransformTask(compilation).configureJvmTask(
177 compilation.compileDependencyFiles,
178 compilation.compileAllTaskName,
179 transformedClassesDir,
180 originalClassesDirs,
181 config
182 )
183 }
184 KotlinPlatformType.js -> {
185 if (!config.transformJs) return@compilations // skip when transformation is turned off
186 project.createJsTransformTask(compilation).configureJsTask(
187 compilation.compileAllTaskName,
188 transformedClassesDir,
189 originalClassesDirs,
190 config
191 )
192 }
193 else -> error("Unsupported transformation platform '${target.platformType}'")
194 }
195 //now transformTask is responsible for compiling this source set into the classes directory
196 classesDirs.setFrom(transformedClassesDir)
197 classesDirs.builtBy(transformTask)
198 (tasks.findByName(target.artifactsTaskName) as? Jar)?.apply {
199 setupJarManifest(multiRelease = config.variant.toVariant() == Variant.BOTH)
200 }
201 // test should compile and run against original production binaries
202 if (compilationType == CompilationType.TEST) {
203 val mainCompilation =
204 compilation.target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME)
205 val originalMainClassesDirs = project.files(
206 // use Callable because there is no guarantee that main is configured before test
207 Callable { originalDirsByCompilation[mainCompilation]!! }
208 )
209
210 (tasks.findByName(compilation.compileKotlinTaskName) as? AbstractCompile)?.classpath =
211 originalMainClassesDirs + compilation.compileDependencyFiles - mainCompilation.output.classesDirs
212
213 (tasks.findByName("${target.name}${compilation.name.capitalize()}") as? Test)?.classpath =
214 originalMainClassesDirs + (compilation as KotlinCompilationToRunnableFiles).runtimeDependencyFiles - mainCompilation.output.classesDirs
215
216 compilation.compileKotlinTask.doFirst {
217 compilation.kotlinOptions.addFriendPaths(originalMainClassesDirs)
218 }
219 }
220 }
221 }
222 }
223
Projectnull224 fun Project.sourceSetsByCompilation(): Map<KotlinSourceSet, List<KotlinCompilation<*>>> {
225 val sourceSetsByCompilation = hashMapOf<KotlinSourceSet, MutableList<KotlinCompilation<*>>>()
226 withKotlinTargets { target ->
227 target.compilations.forEach { compilation ->
228 compilation.allKotlinSourceSets.forEach { sourceSet ->
229 sourceSetsByCompilation.getOrPut(sourceSet) { mutableListOf() }.add(compilation)
230 }
231 }
232 }
233 return sourceSetsByCompilation
234 }
235
Projectnull236 fun Project.configureMultiplatformPluginDependencies(version: String) {
237 if (rootProject.findProperty("kotlin.mpp.enableGranularSourceSetsMetadata").toString().toBoolean()) {
238 val mainConfigurationName = project.extensions.getByType(KotlinMultiplatformExtension::class.java).sourceSets
239 .getByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME)
240 .compileOnlyConfigurationName
241 dependencies.add(mainConfigurationName, getAtomicfuDependencyNotation(Platform.MULTIPLATFORM, version))
242
243 val testConfigurationName = project.extensions.getByType(KotlinMultiplatformExtension::class.java).sourceSets
244 .getByName(KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME)
245 .implementationConfigurationName
246 dependencies.add(testConfigurationName, getAtomicfuDependencyNotation(Platform.MULTIPLATFORM, version))
247
248 // For each source set that is only used in Native compilations, add an implementation dependency so that it
249 // gets published and is properly consumed as a transitive dependency:
250 sourceSetsByCompilation().forEach { (sourceSet, compilations) ->
251 val isSharedNativeSourceSet = compilations.all {
252 it.platformType == KotlinPlatformType.common || it.platformType == KotlinPlatformType.native
253 }
254 if (isSharedNativeSourceSet) {
255 val configuration = sourceSet.implementationConfigurationName
256 dependencies.add(configuration, getAtomicfuDependencyNotation(Platform.MULTIPLATFORM, version))
257 }
258 }
259 } else {
260 sourceSetsByCompilation().forEach { (sourceSet, compilations) ->
261 val platformTypes = compilations.map { it.platformType }.toSet()
262 val compilationNames = compilations.map { it.compilationName }.toSet()
263 if (compilationNames.size != 1)
264 error("Source set '${sourceSet.name}' of project '$name' is part of several compilations $compilationNames")
265 val compilationType = compilationNames.single().compilationNameToType()
266 ?: return@forEach // skip unknown compilations
267 val platform =
268 if (platformTypes.size > 1) Platform.MULTIPLATFORM else // mix of platform types -> "common"
269 when (platformTypes.single()) {
270 KotlinPlatformType.common -> Platform.MULTIPLATFORM
271 KotlinPlatformType.jvm, KotlinPlatformType.androidJvm -> Platform.JVM
272 KotlinPlatformType.js -> Platform.JS
273 KotlinPlatformType.native -> Platform.NATIVE
274 }
275 val configurationName = when {
276 // impl dependency for native (there is no transformation)
277 platform == Platform.NATIVE -> sourceSet.implementationConfigurationName
278 // compileOnly dependency for main compilation (commonMain, jvmMain, jsMain)
279 compilationType == CompilationType.MAIN -> sourceSet.compileOnlyConfigurationName
280 // impl dependency for tests
281 else -> sourceSet.implementationConfigurationName
282 }
283 dependencies.add(configurationName, getAtomicfuDependencyNotation(platform, version))
284 }
285 }
286 }
287
Projectnull288 fun Project.configureTransformTasks(
289 testTaskName: String,
290 createTransformTask: (sourceSet: SourceSet, transformedDir: File, originalDir: FileCollection) -> Task
291 ) {
292 val config = config
293 sourceSets.all { sourceSet ->
294 val compilationType = sourceSet.name.sourceSetNameToType()
295 ?: return@all // skip unknown types
296 val classesDirs = (sourceSet.output.classesDirs as ConfigurableFileCollection).from as Collection<Any>
297 // make copy of original classes directory
298 val originalClassesDirs: FileCollection = project.files(classesDirs.toTypedArray()).filter { it.exists() }
299 (sourceSet as ExtensionAware).extensions.add(ORIGINAL_DIR_NAME, originalClassesDirs)
300 val transformedClassesDir =
301 project.buildDir.resolve("classes/atomicfu/${sourceSet.name}")
302 // make transformedClassesDir the source path for output.classesDirs
303 (sourceSet.output.classesDirs as ConfigurableFileCollection).setFrom(transformedClassesDir)
304 val transformTask = createTransformTask(sourceSet, transformedClassesDir, originalClassesDirs)
305 //now transformTask is responsible for compiling this source set into the classes directory
306 sourceSet.compiledBy(transformTask)
307 (tasks.findByName(sourceSet.jarTaskName) as? Jar)?.apply {
308 setupJarManifest(multiRelease = config.variant.toVariant() == Variant.BOTH)
309 }
310 // test should compile and run against original production binaries
311 if (compilationType == CompilationType.TEST) {
312 val mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
313 val originalMainClassesDirs = project.files(
314 // use Callable because there is no guarantee that main is configured before test
315 Callable { (mainSourceSet as ExtensionAware).extensions.getByName(ORIGINAL_DIR_NAME) as FileCollection }
316 )
317
318 (tasks.findByName(testTaskName) as? AbstractCompile)?.run {
319 classpath =
320 originalMainClassesDirs + sourceSet.compileClasspath - mainSourceSet.output.classesDirs
321
322 (this as? KotlinCompile<*>)?.doFirst {
323 kotlinOptions.addFriendPaths(originalMainClassesDirs)
324 }
325 }
326
327 // todo: fix test runtime classpath for JS?
328 (tasks.findByName(JavaPlugin.TEST_TASK_NAME) as? Test)?.classpath =
329 originalMainClassesDirs + sourceSet.runtimeClasspath - mainSourceSet.output.classesDirs
330 }
331 }
332 }
333
toVariantnull334 fun String.toVariant(): Variant = enumValueOf(toUpperCase(Locale.US))
335
336 fun Project.createJvmTransformTask(compilation: KotlinCompilation<*>): AtomicFUTransformTask =
337 tasks.create(
338 "transform${compilation.target.name.capitalize()}${compilation.name.capitalize()}Atomicfu",
339 AtomicFUTransformTask::class.java
340 )
341
342 fun Project.createJsTransformTask(compilation: KotlinCompilation<*>): AtomicFUTransformJsTask =
343 tasks.create(
344 "transform${compilation.target.name.capitalize()}${compilation.name.capitalize()}Atomicfu",
345 AtomicFUTransformJsTask::class.java
346 )
347
348 fun Project.createJvmTransformTask(sourceSet: SourceSet): AtomicFUTransformTask =
349 tasks.create(sourceSet.getTaskName("transform", "atomicfuClasses"), AtomicFUTransformTask::class.java)
350
351 fun Project.createJsTransformTask(sourceSet: SourceSet): AtomicFUTransformJsTask =
352 tasks.create(sourceSet.getTaskName("transform", "atomicfuJsFiles"), AtomicFUTransformJsTask::class.java)
353
354 fun AtomicFUTransformTask.configureJvmTask(
355 classpath: FileCollection,
356 classesTaskName: String,
357 transformedClassesDir: File,
358 originalClassesDir: FileCollection,
359 config: AtomicFUPluginExtension
360 ): ConventionTask =
361 apply {
362 dependsOn(classesTaskName)
363 classPath = classpath
364 inputFiles = originalClassesDir
365 outputDir = transformedClassesDir
366 variant = config.variant
367 verbose = config.verbose
368 }
369
AtomicFUTransformJsTasknull370 fun AtomicFUTransformJsTask.configureJsTask(
371 classesTaskName: String,
372 transformedClassesDir: File,
373 originalClassesDir: FileCollection,
374 config: AtomicFUPluginExtension
375 ): ConventionTask =
376 apply {
377 dependsOn(classesTaskName)
378 inputFiles = originalClassesDir
379 outputDir = transformedClassesDir
380 verbose = config.verbose
381 }
382
setupJarManifestnull383 fun Jar.setupJarManifest(multiRelease: Boolean) {
384 if (multiRelease) {
385 manifest.attributes.apply {
386 put("Multi-Release", "true")
387 }
388 }
389 }
390
391 val Project.sourceSets: SourceSetContainer
392 get() = convention.getPlugin(JavaPluginConvention::class.java).sourceSets
393
394 class AtomicFUPluginExtension(pluginVersion: String?) {
395 var dependenciesVersion = pluginVersion
396 var transformJvm = true
397 var transformJs = true
398 var variant: String = "FU"
399 var verbose: Boolean = false
400 }
401
402 @CacheableTask
403 open class AtomicFUTransformTask : ConventionTask() {
404 @PathSensitive(PathSensitivity.RELATIVE)
405 @InputFiles
406 lateinit var inputFiles: FileCollection
407
408 @OutputDirectory
409 lateinit var outputDir: File
410
411 @Classpath
412 @InputFiles
413 lateinit var classPath: FileCollection
414
415 @Input
416 var variant = "FU"
417 @Input
418 var verbose = false
419
420 @TaskAction
transformnull421 fun transform() {
422 val cp = classPath.files.map { it.absolutePath }
423 inputFiles.files.forEach { inputDir ->
424 AtomicFUTransformer(cp, inputDir, outputDir).let { t ->
425 t.variant = variant.toVariant()
426 t.verbose = verbose
427 t.transform()
428 }
429 }
430 }
431 }
432
433 @CacheableTask
434 open class AtomicFUTransformJsTask : ConventionTask() {
435 @PathSensitive(PathSensitivity.RELATIVE)
436 @InputFiles
437 lateinit var inputFiles: FileCollection
438
439 @OutputDirectory
440 lateinit var outputDir: File
441 @Input
442 var verbose = false
443
444 @TaskAction
transformnull445 fun transform() {
446 inputFiles.files.forEach { inputDir ->
447 AtomicFUTransformerJS(inputDir, outputDir).let { t ->
448 t.verbose = verbose
449 t.transform()
450 }
451 }
452 }
453 }
454