• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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